3.7 - Fragment Discard and Transparency

What We're Learning
So far in this series, every fragment we've processed has been drawn to the screen. We've colored them, textured them, and lit them - but we've always drawn them. What if you don't want to draw certain fragments at all? What if you want holes, cutouts, or complex silhouettes without adding extra geometry?
This is where the discard statement comes in - one of the most powerful and distinctive tools in a fragment shader. With a single keyword, you can make a fragment disappear as if it never existed. No color written, no depth written, nothing. It is the fragment shader equivalent of an invisibility cloak.
However, transparency isn't just about deleting pixels. There is also alpha blending - where fragments partially show through to what's behind them. These two approaches - discard and blending - solve different problems and come with very different trade-offs in the deferred or forward rendering pipelines.
In this article, you will learn:
The Discard Statement: How to immediately terminate a pixel's processing.
Alpha Testing: Creating hard cutouts for foliage, fences, and grates.
Discard vs. Blending: Why discard gives you correct depth sorting for free, while blending creates sorting headaches.
Performance: Why "doing nothing" (discarding) can sometimes be slower than drawing.
Bevy's Alpha Modes: How to configure StandardMaterial or custom materials for different types of transparency.
Concept: Forward vs. Deferred Rendering
To understand why transparency is tricky, we need to understand the two main ways game engines render scenes.
Forward Rendering: The classic approach. The GPU draws each object one by one and calculates lighting immediately.
Pros: Handles transparency (blending) well.
Cons: Gets very slow if you have hundreds of dynamic lights.
Deferred Rendering: The modern approach for high-fidelity games. The GPU first renders the
geometrydata (Position, Normal, Color) of every pixel to a buffer (the "G-Buffer"), and calculates lighting for the entire screen at the end.Pros: Can handle thousands of lights efficiently.
Cons: Hates transparency. Because the G-Buffer only stores data for the single closest object per pixel, it can't store "multiple transparent layers" (like glass in front of a car).
Why this matters: When you use discard, the fragment remains "opaque" - it's either there or it isn't. This works perfectly in both pipelines.
When you use alpha blending, the engine usually has to break out of the Deferred pipeline and draw that specific object using Forward rendering at the very end. This is why "cutout" shaders (discard) are often much faster than "transparent" shaders (blend).
Understanding Fragment Discard
Let's start with the simplest and most dramatic tool: the discard statement.
What discard Does
The discard statement immediately terminates the execution of the fragment shader. The fragment is not written to any output - not the color buffer, not the depth buffer, nothing. The GPU simply stops working on that specific pixel for that specific triangle.
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Calculate if we should discard based on arbitrary logic
let should_hide = in.uv.y > 0.5;
if should_hide {
discard; // STOP. Do not pass Go. Do not write pixel.
}
// This code only runs if we didn't discard
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
Critical properties of discard:
Immediate Termination: Code written after the
discardstatement inside that branch does not execute.No Depth Write: This is the most important feature. If a fragment is discarded, it does not update the depth buffer. Objects behind this fragment will be rendered normally, as if this geometry wasn't there.
Binary Visibility: A fragment is either there (100% opacity) or it isn't (0% opacity). There is no "50% visible" with discard.
Visual Comparison

Basic Example: The Checkerboard
The simplest procedural use case is a checkerboard. We don't need a texture for this; we can calculate it using UVs.
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Create a 10x10 grid pattern
let tiles = floor(in.uv * 10.0);
// Check if the sum of x and y indices is even or odd
let checker = tiles.x + tiles.y;
// modulo 2.0 to alternate
if (checker % 2.0) > 0.5 {
discard;
}
// Render the remaining squares as red
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
This creates a lattice where half the squares are completely transparent - you can see through them to the world behind - and half are solid red.
Performance: When to Discard
It is important to understand that discard is not like a return. It aborts the thread.
Optimization Tip: If you know you are going to discard a pixel, do it as early as possible.
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// 1. Check discard condition FIRST
if in.uv.y < 0.1 {
discard;
}
// 2. Perform expensive lighting/math AFTER
// This code won't run for the bottom 10% of the UVs, saving GPU power.
let lighting = calculate_complex_pbr_lighting(in);
return vec4<f32>(lighting, 1.0);
}
Discard and Depth Testing
One of the key features of discard is how it interacts with the depth buffer.
Normally, when a fragment shader runs, the GPU might update the Depth Buffer to say "there is an object here at distance Z." If we discard, that update never happens. This means we don't need to worry about the "draw order" of our triangles.

This makes discard the perfect tool for foliage, chain-link fences, and grates.
Alpha Testing with Conditional Discard
The most common use of discard is Alpha Testing (also called "Cutout" transparency). This technique uses a texture's alpha channel to decide which pixels to keep and which to delete.
The Classic Alpha Test Pattern
Imagine you have a texture of a chain-link fence. The metal parts have an alpha of 1.0 (opaque), and the gaps have an alpha of 0.0 (transparent).
// Define bindings for Group 2 (Material Data)
@group(2) @binding(0) var base_texture: texture_2d<f32>;
@group(2) @binding(1) var base_sampler: sampler;
struct AlphaCutoffMaterial {
cutoff_threshold: f32, // Usually 0.5
}
@group(2) @binding(2) var<uniform> material: AlphaCutoffMaterial;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// 1. Sample the texture
let color = textureSample(base_texture, base_sampler, in.uv);
// 2. Alpha Test: Compare texture alpha against our threshold
if color.a < material.cutoff_threshold {
discard;
}
// 3. Render the pixel (opaque)
return color;
}
How it works:
We sample the texture at the current UV.
We check if the alpha value is too low (e.g., < 0.5).
If it is, we discard. If not, we draw the pixel at full opacity.
Choosing the Right Threshold
The cutoff threshold determines how "strict" the discard is.
0.5: The standard default. Balanced.
0.1: "Generous." Keeps almost everything, even faint wisps. Good for preserving details but might leave "dirty" edges.
0.9: "Strict." Only keeps the most solid parts. Good for shrinking an object visually.
Alpha Channel Gradient: 0.0 ... 0.3 ... 0.5 ... 0.7 ... 1.0
Threshold 0.5 keeps: [------ KEEP -----]
Threshold 0.9 keeps: [KEEP ]
The Problem: Binary Edges
With standard Alpha Testing, there are only two states: visible or invisible. There is no partial transparency. This creates "jagged" or aliased edges, especially when the camera is close.
To fix this cheaply without enabling expensive Alpha Blending, we can use Dithering.
Advanced: Dithered Alpha Testing
Dithering uses a noise pattern to randomly discard pixels near the edge threshold. This fakes transparency by mixing opaque and discarded pixels in a checkerboard-like pattern.
// Helper: 4x4 Bayer Matrix for ordered dithering
fn bayer_dither(position: vec2<f32>) -> f32 {
let x = u32(position.x) % 4u;
let y = u32(position.y) % 4u;
// Hardcoded Bayer matrix normalized to 0.0-1.0
// This creates a balanced noise pattern
let index = y * 4u + x;
var bayer = array<f32, 16>(
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
);
return bayer[index];
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let color = textureSample(base_texture, base_sampler, in.uv);
// Get a dither value based on SCREEN position (in.position)
// -0.5 to +0.5 range centered around 0
let dither_noise = bayer_dither(in.position.xy) - 0.5;
// Modulate the threshold slightly per pixel
// "0.1" controls how wide the "fuzzy" edge is
let noisy_threshold = material.cutoff_threshold + (dither_noise * 0.1);
if color.a < noisy_threshold {
discard;
}
return color;
}
This effectively "softens" the hard edge. From a distance, the dithered pixels blend together in your eye, making the edge look smooth.
Creating Cutout Effects
Alpha testing is the standard technique for specific types of objects in games.
Leaves and Foliage
Leaves are almost always rendered as simple quads (rectangles) with a texture.
A tree might have 10,000 leaves. Sorting them back-to-front for Alpha Blending would crush the CPU. With discard, the Z-buffer handles the sorting automatically.
Fences and Grates
Chain-link fences are just flat planes with a repeating texture.
discard creates sharp, metallic edges. Semi-transparent blending would make the metal look like ghost-fence.
Procedural Holes
You don't always need a texture. You can punch holes mathematically using discard.
// Example: Swiss Cheese Shader
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// Create a repeating grid
let grid_uv = fract(in.uv * 10.0);
// Calculate distance from center of the grid cell
let dist = length(grid_uv - vec2<f32>(0.5));
// If inside the circle radius, discard
if dist < 0.4 {
discard;
}
return vec4<f32>(1.0, 1.0, 0.0, 1.0); // Yellow cheese
}
Discard vs. Alpha Blending Trade-offs
Now we reach the critical question: when should you use Alpha Testing (discard) versus standard Alpha Blending?
In Bevy, you choose between these behaviors using the AlphaMode setting on your material, but understanding what happens under the hood is vital for performance and visual quality.
Alpha Blending Recap
Before comparing, let's remember what Alpha Blending is. instead of "deleting" the pixel, the GPU mixes it with the color that is already on the screen behind it.
// Alpha Blending (No discard)
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let color = textureSample(base_texture, base_sampler, in.uv);
// Return color with alpha.
// The GPU hardware handles the mixing automatically.
return color;
}
Equation: FinalColor = (SourceColor * Alpha) + (DestinationColor * (1.0 - Alpha))
Comparison Table
| Feature | Alpha Testing (discard) | Alpha Blending (mix) |
| Edge Quality | Sharp, jagged edges (Pixelated). | Smooth, soft edges (Anti-aliased). |
| Transparency | Binary (On/Off). | Continuous (0% to 100%). |
| Depth Buffer | Writes to depth. | Usually Read-Only. |
| Sorting | Not Required. | Required (Back-to-Front). |
| Use Cases | Foliage, Fences, Grates. | Glass, Water, Holograms, Smoke. |
| Bevy Mode | AlphaMode::Mask(0.5) | AlphaMode::Blend |
The Sorting Problem
The single biggest downside of Alpha Blending is that draw order matters.
If you draw a red glass pane in front of a blue glass pane:
Correct (Back-to-Front): Draw Blue, then Draw Red on top. Result: Purple.
Incorrect (Front-to-Back): Draw Red. Because it is transparent, it doesn't write to the depth buffer. Then Draw Blue. Since nothing wrote to the depth buffer, Blue draws on top of Red. Result: Blue looks like it is in front of Red.
How Bevy handles this:
Bevy's renderer automatically calculates the distance from the camera to every object with AlphaMode::Blend and sorts them every frame.
Limitation 1: It sorts objects, not triangles. If a large complex mesh intersects itself, it will glitch.
Limitation 2: It costs CPU time to sort thousands of objects every frame.
Why discard wins here:
Because discard allows the opaque pixels to write to the depth buffer, order does not matter. You can draw a forest of 10,000 trees in any order, and the depth buffer ensures the trees behind are correctly hidden by the trees in front.
Performance Considerations
When choosing a transparency mode, it helps to view them in a hierarchy of cost.
1. Opaque (AlphaMode::Opaque) - The Speed King
This is the fastest mode. Modern GPUs use "Early-Z" optimization: before running your pixel shader, they check the depth buffer. If a wall is in front of the pixel, the GPU skips the shader entirely.
Verdict: Use this whenever possible.
2. Discard / Alpha Mask (AlphaMode::Mask) - The Optimization Breaker
You might think: "Discarding pixels means I don't write them, so it should be faster than Opaque!"
Not always!
To know if a pixel should be discarded, the GPU must run the fragment shader. This forces the GPU to disable Early-Z optimization for that object. It calculates every single pixel, even the ones hidden behind other objects, just to check if they need to be deleted.
Verdict: Slower than Opaque, but much faster than Blending.
3. Alpha Blending (AlphaMode::Blend) - The Bandwidth Eater
This is the most expensive mode.
CPU Cost: Bevy must calculate distances and sort every object every frame.
GPU Cost: For every transparent layer (e.g., looking through smoke), the GPU must read the current color from memory, do math, and write it back. This "Read-Modify-Write" loop eats up Memory Bandwidth, which is often the bottleneck on mobile devices and integrated graphics.
Verdict: Use sparingly.
Summary Rule of Thumb
If it's solid, use
AlphaMode::Opaque.If it has holes but hard edges (leaves, fences), use
AlphaMode::Mask.Only use
AlphaMode::Blendif you absolutely need partial transparency (glass, water).
Bevy's Alpha Mode Configuration
In Bevy, you control this behavior directly on your material struct. Whether you use StandardMaterial or a custom Material, the API is the same.
use bevy::prelude::*;
impl Material for MyCustomMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/my_shader.wgsl".into()
}
// This function tells Bevy's pipeline how to handle your shader
fn alpha_mode(&self) -> AlphaMode {
// Option A: Opaque (Default)
// Fastest. Writes Depth. Ignores alpha channel.
AlphaMode::Opaque
// Option B: Alpha Mask / Testing
// Writes Depth. Enables `discard` behavior in the generic shader.
// The f32 value is the cutoff threshold (usually 0.5).
AlphaMode::Mask(0.5)
// Option C: Alpha Blending
// No Depth Write. CPU Sorting enabled. Smooth edges.
AlphaMode::Blend
// Option D: Additive (Good for glowing particles/fire)
AlphaMode::Add
}
}
Note: If you are writing a completely custom fragment shader (like we are in the next section), setting
AlphaMode::Maskin Rust does not automatically add thediscardkeyword to your WGSL code. You must write theif (alpha < threshold) { discard; }logic yourself!
The AlphaMode in Rust primarily tells the render pipeline:
Whether to enable Depth Writes.
Whether to enable the CPU Sorter.
Which Blend State (Mix, Add, Multiply) to configure on the GPU.
Complete Example: The Teleportation Chamber
We will build a sci-fi teleportation scene that demonstrates both uses of transparency in a single project. This approach highlights exactly when to use one technique over the other.
Safety Grates (Alpha Mask): We will create a perforated metal floor and wall. Since metal is solid (it's either there or it isn't), we use Alpha Testing. We will also disable backface culling so we can see the grates from both sides.
The Hero (Blend + Discard): We will create a holographic character. Since holograms are made of light, they are semi-transparent (Alpha Blending). However, when the character teleports, they will "dematerialize" using a noise-based Discard effect.
This example relies entirely on procedural math (sine waves and hash functions), so no texture assets are required.
1. The Grate Material (Alpha Testing)
This material generates a "Perforated Metal" look. We use AlphaMode::Mask because we want the edges of the holes to be razor-sharp.
The Shader (assets/shaders/d03_07_simple_grate.wgsl)
In the fragment shader, we divide the UV space into a grid. Inside each cell, we calculate the distance from the center. If that distance is smaller than our hole_radius, we discard the pixel.
To make it look 3D, we add a fake "bevel" highlight around the rim of the hole using smoothstep. This makes the flat geometry feel like a thick metal plate.
#import bevy_pbr::forward_io::VertexOutput
struct GrateMaterial {
color: vec4<f32>,
bar_width: f32, // Controls hole size (0.0 to 0.5)
_padding: vec3<f32>,
}
@group(2) @binding(0) var<uniform> material: GrateMaterial;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// 1. Create a repeating grid
// We offset by 0.5 so the center of the UV cell is (0,0)
let grid_uv = fract(in.uv * 20.0) - 0.5;
// 2. Calculate distance from the center of the cell
let dist = length(grid_uv);
// 3. Alpha Test (The Hole)
// If we are inside the circle radius defined by bar_width, discard.
// This creates "Swiss Cheese" perforated metal.
if dist < material.bar_width {
discard;
}
// 4. Fake "Bevel" Lighting
// We make the pixels right next to the hole brighter.
// This simulates light catching the rim of the drilled hole,
// making the flat plane look like it has thickness.
let bevel = smoothstep(material.bar_width, material.bar_width + 0.1, dist);
// Mix a dark shadow color with the material color based on the bevel
let final_color = mix(material.color.rgb * 0.5, material.color.rgb, bevel);
return vec4<f32>(final_color, material.color.a);
}
The Rust Code (src/materials/d03_07_simple_grate.rs)
In the Rust material, we do two important things:
We set
AlphaMode::Mask(0.5)to enable the cutouts.We override the
specializemethod to disablecull_mode. This tells the GPU to draw the triangles even if they are facing away from the camera, allowing us to see the back of the wall through the front of the wall.
use bevy::pbr::{MaterialPipeline, MaterialPipelineKey};
use bevy::prelude::*;
use bevy::render::mesh::MeshVertexBufferLayoutRef;
use bevy::render::render_resource::{
AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError,
};
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct GrateMaterial {
#[uniform(0)]
pub color: LinearRgba,
#[uniform(0)]
pub hole_radius: f32, // Renamed for clarity (was bar_width)
#[uniform(0)]
pub _padding: Vec3,
}
impl Default for GrateMaterial {
fn default() -> Self {
Self {
color: LinearRgba::new(0.6, 0.6, 0.7, 1.0), // Steel Blue-Grey
hole_radius: 0.35, // Nice perforation size
_padding: Vec3::ZERO,
}
}
}
impl Material for GrateMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/d03_07_simple_grate.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Mask(0.5)
}
// ENABLE DOUBLE-SIDED RENDERING
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
_key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
// None = Draw both front and back faces
descriptor.primitive.cull_mode = None;
Ok(())
}
}
Don't forget to add it to src/materials/mod.rs:
pub mod d03_07_simple_grate;
2. The Teleport Material (Blend + Discard)
This material renders the "Hero". It combines three distinct effects:
Hologram Scanlines: A sine wave moving vertically to simulate projection interference.
Rim Lighting: A Fresnel calculation to make the edges of the model glow, enhancing the 3D volume.
Teleport Dissolve: A noise-based
discardeffect that eats away the geometry.
The Shader (assets/shaders/d03_07_teleport_body.wgsl)
Note that we calculate the Fresnel effect using the dot product between the view vector (camera direction) and the normal vector. This adds that classic sci-fi "edge glow."
The dissolve effect happens before the lighting. If the noise value is below our threshold, we discard immediately. If the pixel survives, we then apply the holographic blending.
#import bevy_pbr::forward_io::VertexOutput
#import bevy_pbr::mesh_view_bindings::view
struct TeleportMaterial {
color: vec4<f32>,
dissolve_amount: f32,
time: f32,
_padding: vec2<f32>,
}
@group(2) @binding(0) var<uniform> material: TeleportMaterial;
fn hash(p: vec2<f32>) -> f32 {
let n = dot(p, vec2<f32>(12.9898, 78.233));
return fract(sin(n) * 43758.5453);
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
// 1. Dissolve (Discard)
let noise = hash(in.uv * 50.0 + material.time);
if noise < material.dissolve_amount {
discard;
}
// 2. Hologram Scanlines
let scanline = sin(in.world_position.y * 50.0 - material.time * 5.0);
let scan_strength = 0.7 + 0.3 * scanline;
// 3. Fresnel Rim Light (3D effect)
let view_dir = normalize(view.world_position.xyz - in.world_position.xyz);
let normal = normalize(in.world_normal);
let fresnel = 1.0 - max(dot(view_dir, normal), 0.0);
let rim_glow = pow(fresnel, 3.0) * 2.0;
// 4. Combine
let final_alpha = material.color.a * scan_strength + rim_glow;
let final_color = material.color.rgb + vec3<f32>(rim_glow);
return vec4<f32>(final_color, final_alpha);
}
The Rust Code (src/materials/d03_07_teleport_body.rs)
We use AlphaMode::Blend. Even though we use discard inside the shader, we need Blending enabled to make the hologram look like semi-transparent light.
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct TeleportMaterial {
#[uniform(0)]
pub color: LinearRgba,
#[uniform(0)]
pub dissolve_amount: f32,
#[uniform(0)]
pub time: f32,
#[uniform(0)]
pub _padding: Vec2,
}
impl Default for TeleportMaterial {
fn default() -> Self {
Self {
color: LinearRgba::new(0.0, 1.0, 1.0, 0.3), // Cyan, low opacity
dissolve_amount: 0.0,
time: 0.0,
_padding: Vec2::ZERO,
}
}
}
impl Material for TeleportMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/d03_07_teleport_body.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Blend
}
}
Don't forget to add it to src/materials/mod.rs:
pub mod d03_07_teleport_body;
3. The Demo Scene (src/demos/d03_07_teleport_demo.rs)
This demo sets up the chamber, spawns the hero, and handles the logic. We add an Orbit Camera so you can rotate around the scene and inspect the transparency sorting from all angles.
When you press Space, we animate the dissolve_amount uniform. The hero dissolves, moves to a new random location while invisible, and then re-materializes.
use crate::materials::d03_07_simple_grate::GrateMaterial;
use crate::materials::d03_07_teleport_body::TeleportMaterial;
use bevy::prelude::*;
pub fn run() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MaterialPlugin::<GrateMaterial>::default())
.add_plugins(MaterialPlugin::<TeleportMaterial>::default())
.add_systems(Startup, setup)
.add_systems(
Update,
(
handle_teleport_input,
animate_teleport,
update_time,
orbit_camera,
),
)
.run();
}
#[derive(Component)]
struct Hero;
#[derive(Component)]
struct OrbitCamera {
radius: f32,
angle: f32,
}
#[derive(Component)]
struct TeleportState {
target_pos: Vec3,
is_teleporting: bool,
dissolve_progress: f32,
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut grate_materials: ResMut<Assets<GrateMaterial>>,
mut teleport_materials: ResMut<Assets<TeleportMaterial>>,
) {
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 5.0, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
OrbitCamera {
radius: 12.0,
angle: 0.0,
},
));
// Light
commands.spawn((
PointLight {
intensity: 2_000_000.0,
range: 20.0,
..default()
},
Transform::from_xyz(0.0, 10.0, 0.0),
));
// 1. The Chamber (Perforated Metal)
let wall_mesh = meshes.add(Rectangle::new(8.0, 4.0));
let grate_mat = grate_materials.add(GrateMaterial::default());
// Back Wall
commands.spawn((
Mesh3d(wall_mesh.clone()),
MeshMaterial3d(grate_mat.clone()),
Transform::from_xyz(0.0, 2.0, -4.0),
));
// Front Wall
commands.spawn((
Mesh3d(wall_mesh),
MeshMaterial3d(grate_mat),
Transform::from_xyz(0.0, 2.0, 4.0),
));
// 2. The Hero (Hologram)
commands.spawn((
Mesh3d(meshes.add(Capsule3d::default())),
MeshMaterial3d(teleport_materials.add(TeleportMaterial::default())),
Transform::from_xyz(0.0, 1.0, 0.0),
Hero,
TeleportState {
target_pos: Vec3::ZERO,
is_teleporting: false,
dissolve_progress: 0.0,
},
));
// UI
commands.spawn((
Text::new(
"CONTROLS:\n\
[Space] Teleport Hero\n\
[Left/Right] Orbit Camera",
),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn update_time(time: Res<Time>, mut materials: ResMut<Assets<TeleportMaterial>>) {
for (_, mat) in materials.iter_mut() {
mat.time = time.elapsed_secs();
}
}
fn orbit_camera(
time: Res<Time>,
keyboard: Res<ButtonInput<KeyCode>>,
mut query: Query<(&mut Transform, &mut OrbitCamera)>,
) {
let speed = 2.0;
for (mut transform, mut orbit) in query.iter_mut() {
if keyboard.pressed(KeyCode::ArrowLeft) {
orbit.angle -= speed * time.delta_secs();
}
if keyboard.pressed(KeyCode::ArrowRight) {
orbit.angle += speed * time.delta_secs();
}
let x = orbit.radius * orbit.angle.sin();
let z = orbit.radius * orbit.angle.cos();
transform.translation = Vec3::new(x, 5.0, z);
transform.look_at(Vec3::new(0.0, 1.0, 0.0), Vec3::Y);
}
}
fn handle_teleport_input(
keyboard: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut TeleportState, With<Hero>>,
) {
if keyboard.just_pressed(KeyCode::Space) {
for mut state in query.iter_mut() {
if !state.is_teleporting {
state.is_teleporting = true;
state.dissolve_progress = 0.0;
let x = (rand::random::<f32>() - 0.5) * 6.0;
state.target_pos = Vec3::new(x, 1.0, 0.0);
}
}
}
}
fn animate_teleport(
time: Res<Time>,
mut materials: ResMut<Assets<TeleportMaterial>>,
mut query: Query<(
&mut Transform,
&mut TeleportState,
&MeshMaterial3d<TeleportMaterial>,
)>,
) {
for (mut transform, mut state, handle) in query.iter_mut() {
if state.is_teleporting {
state.dissolve_progress += time.delta_secs() * 2.0;
if let Some(material) = materials.get_mut(handle) {
if state.dissolve_progress < 1.0 {
// Dissolve
material.dissolve_amount = state.dissolve_progress;
} else {
// Move & Reappear
if state.dissolve_progress < 1.1 {
transform.translation = state.target_pos;
}
material.dissolve_amount = 2.0 - state.dissolve_progress;
}
}
if state.dissolve_progress >= 2.0 {
state.is_teleporting = false;
if let Some(material) = materials.get_mut(handle) {
material.dissolve_amount = 0.0;
}
}
}
}
}
Don't forget to add it to src/demos/mod.rs:
pub mod d03_07_teleport_demo;
And register it in src/main.rs:
Demo {
number: "3.7",
title: "Fragment Discard and Transparency",
run: demos::d03_07_teleport_demo::run,
},
Running the Demo
Controls
| Key | Action |
| Space | Teleport the Hero to a random location |
| Arrow Left/Right | Orbit the camera around the scene |
What You're Seeing

The Grate (Alpha Mask): Notice the sharp, pixel-perfect edges of the holes. As you orbit the camera, look through the holes of the front wall to see the back wall. Because we use
discard, the depth buffer works perfectly, and the sorting is correct.The Hologram (Alpha Blend): The hero looks like a volume of light. Notice the scanlines and the rim glow.
The Teleport (Discard): When you press Space, the hero doesn't fade out evenly; they "burn away" into static. This demonstrates that you can use
discardinside a blended material to create interesting erosion effects.
Key Takeaways
Discard is distinct from Alpha: Transparency (alpha < 1.0) blends colors. Discard deletes pixels.
Use Masks for Structure: For objects like fences, grates, or foliage, use
AlphaMode::Mask(Discard). It's sharp, performant, and handles depth correctly.Use Blend for Light: For objects that represent glass, ghosts, or energy, use
AlphaMode::Blend.Combine them for Effects: You can use
discardinside a Blended shader to create dissolve, burn, or teleportation effects.Disable Culling for Thin Objects: If you have a single-plane object (like a grate or leaf) that should be visible from both sides, remember to disable backface culling in the
specializemethod.
What's Next?
We've mastered how to draw pixels, how to move them, and how to delete them. Now it's time to look at the final stage of the pipeline: effects that happen after the scene is drawn.
Next up: 3.8 - Advanced Color Techniques
Quick Reference
Transparency Comparison
Choosing the right AlphaMode is the most important optimization you can make for transparent objects.
| Feature | Opaque | Alpha Mask (Discard) | Alpha Blend |
| Best For... | Rocks, Walls, Characters | Foliage, Fences, Grates | Glass, Water, Holograms |
| Visuals | Solid | Solid with holes (Hard edges) | See-through (Soft edges) |
| Depth Buffer | Writes Depth | Writes Depth (for opaque pixels) | Read-Only (usually) |
| Sorting | Not Required | Not Required | Required (CPU Intensive) |
| Performance | 🟢 Fastest (Uses Early-Z) | 🟡 Medium (Breaks Early-Z) | 🔴 Slowest (Overdraw + Sorting) |
Performance Heuristics
The Early-Z Penalty: Using
discardin a shader usually disables the GPU's ability to skip hidden pixels (Early-Z). The shader must run for every pixel to decide if it should be deleted.The Overdraw Penalty: Alpha Blending requires reading and writing to the same pixel memory multiple times. Too many layers of smoke or glass will bottleneck your GPU's memory bandwidth.
The Sorting Penalty: Bevy must sort all Blended objects on the CPU every frame. Avoid thousands of transparent objects.
Common Math for Effects
Circular Hole:
if length(uv - center) < radius { discard; }Grid:
if step(width, sin(uv * scale)) < 0.5 { discard; }Dissolve:
if noise(uv) < threshold { discard; }Rim Light:
1.0 - dot(view_dir, normal)





