owned this note changed 5 days ago
Published Linked with GitHub

2d Transforms & Sorting

The purpose of this working group is to improve Bevy's 2D support by adding a specialized 2D transform.

There's an abnormally large number of closely linked PRs in development that touch this topic: A 2d transform PR, a UI transform PR, a y-sorting PR, as well as the new tilemap renderer. So part of the purpose of this document is cross-initiative coordination.

Proposal

  • Merge the ui transform PR basically as is.
  • New PR: Replace ZIndex and GlobalZIndex with a field on UiTransform.
  • Clean up and merge the 2d transform PR.
  • Merge the y-sorting PR, modified so that ZIndex and YSort are fields of Transform2d (leave out SortBias for now) and ZIndex actually effects the GlobalTransform.

Demonstration

Here's an overview of what this would look like. (WIP)

#[derive(Component, Default)] pub struct Transform2d { /// X-Y Position translation: Vec2, /// Rotation about Z Axis rotation: Rot2, /// Scale in X and Y scale: f32, /// Z component of position depth: Depth2d, /// Opt-in to y sort y_sort: bool } /// Determines the layer a 2d object is drawn on. #[derive(Default)] pub enum Depth2d { Relative(f32), // Parent layer + i Absolute(f32), // Set layer to i } impl Default for Depth2d { fn default() -> Depth2d { // This default makes children be drawn slightly infront // of their parents. Depth2d::Relative(1.0) } }
  • Mixing 2d and 3d transforms in the same hierarchy is allowed. Both compute GlobalTransform given the parent's GlobalTransform (if it exists).
  • We will use hooks to enforce archetype invariants: Adding one transform type automatically removes the other transform type.
  • Relative values for depth are incorperated into transform prop. depth is effectivly the same as the z position component on Transform3d, but with the option to specify it in absolute terms.
  • Camera will no longer require Transform. Instead Camera3d will require Transform3d and Camera2d will require Transform2d.

Detailed Analysis of Related PRs

PR #19389: 2D Transforms - Core Assessment

What It Actually Does

  • Renames TransformTransform3d (with backward-compatible alias)
  • Introduces Transform2d with 2D-native fields (Vec2, Rot2, f32 scale)
  • Duplicates transform propagation systems for 2D variants
  • Updates all sprite/2D systems to work with new types

Critical Issues Identified from Review

  1. The "Unpleasant" Layering Problem

    • Current PR requires 3D parent entities for Z-layering
    • Forces developers back into 3D thinking for 2D problems
    • Example of problematic workaround:
    ​​​commands.spawn((
    ​​​    Name("Layer five"),
    ​​​    Transform3d::from_translation(Vec3::Z * 5.0), // 3D parent!
    ​​​    children![
    ​​​        (MyComponent, Transform2d::from_translation(Vec2::splat(10.0)))
    ​​​    ]
    ​​​));
    
  2. Incomplete Solution Without Sorting

    • Transform2d alone doesn't solve the core 2D ergonomics problem
    • Still requires manual Z-coordinate management
    • Reviewer consensus: needs sorting solution in same release

Performance Implications

Positive:

  • Smaller memory footprint (Vec2 vs Vec3, Rot2 vs Quat)
  • Reduced computational overhead for 2D operations
  • Better cache locality

Concerns:

  • Code duplication in transform systems
  • Complexity in mixed 2D/3D hierarchies

PR #19463: Sorting Components - Integration Analysis

The Sorting Stack

┌─────────────────┐
│ ZIndex (i32)    │ ← Primary: Layer separation
├─────────────────┤
│ YSort (marker)  │ ← Secondary: Depth illusion  
├─────────────────┤
│ SortBias (f32)  │ ← Tertiary: Fine-tuning
└─────────────────┘

Key Reviewer Concerns

  1. Naming Conflict with UI ZIndex

    • UI already has ZIndex component with different semantics
    • Need resolution strategy before merge
  2. Per-Entity vs Per-Layer Y-Sorting

    • Current: Per-entity YSort marker
    • Concern: May be confusing when mixed with non-Y-sorted entities
    • Reviewer suggestion: Y-sorting as layer property
  3. Missing Hierarchy Propagation

    • No automatic Z-index inheritance
    • Children don't inherit parent's layer by default
    • Community expectation: children should inherit parent's Z-layer

Critical Edge Cases

  • Mixed Y-Sort Entities: Inconsistent behavior when some entities in layer have YSort, others don't
  • Floating Point Precision: Y-sorting reliability with similar Y positions
  • Performance Scaling: CPU-based sorting limits for large entity counts

Synergy Analysis: Why Both PRs Work Better Together

The Perfect Marriage

PR #19463 directly solves PR #19389's biggest problem:

Before (PR #19389 alone):

// Awkward 3D parent workaround
commands.spawn((
    Transform3d::from_translation(Vec3::Z * 5.0),
    children![(Transform2d::default(), SpriteBundle::default())]
));

After (Both PRs):

// Clean, ergonomic 2D solution
commands.spawn((
    Transform2d::from_translation(Vec2::new(x, y)),
    ZIndex::new(5),
    YSort, // Optional depth sorting
    SpriteBundle::default()
));

Integration Recommendation: Unified Transform2d

The proposed Transform2d with integrated depth/sorting fields addresses all core issues:

#[derive(Component, Default)]
pub struct Transform2d {
    translation: Vec2,
    rotation: Rot2, 
    scale: f32,
    depth: Depth2d,    // Integrated Z-layering
    y_sort: bool,      // Integrated Y-sorting
}

Benefits:

  • Single component for all 2D transform needs
  • Eliminates need for separate sorting components
  • Provides both relative and absolute depth control
  • Maintains transform propagation semantics

Migration Strategy & Risk Assessment

Phase 1: Foundation (Immediate)

  1. Resolve naming conflicts - Rename 2D sorting components
  2. Integrate depth into Transform2d - Eliminate 3D parent workaround
  3. Implement hierarchy propagation - Children inherit parent depth by default

Phase 2: Optimization (Follow up PRs)

  1. Performance validation - Benchmark sorting with realistic 2D scenes
  2. GPU-based sorting - For high entity count scenarios
  3. Advanced sorting features - Custom sort keys, spatial partitioning

Critical Success Factors

Must Resolve Before Merge

  1. UI ZIndex naming conflict - Clear disambiguation strategy
  2. Hierarchy propagation - Automatic depth inheritance
  3. Performance benchmarking - Validate sorting overhead acceptable

High-Risk Areas

  1. Breaking changes - Large mechanical refactoring
  2. Performance regression - Additional sorting complexity
  3. User confusion - Dual transform system complexity

Mitigation Strategies

  1. Comprehensive backward compatibility - Transform alias, migration tools
  2. Performance budgets - Clear thresholds for acceptable overhead
  3. Clear documentation - When to use 2D vs 3D transforms

Community Feedback Integration

Key Reviewer Positions

@rparrett: Strong opposition to 3D parent workaround

  • Resolution: Integrated depth in Transform2d eliminates this need

@ickshonpe: Concerned about UI ZIndex conflict

  • Resolution: Rename 2D version or namespace appropriately

@hymm: Y-sorting should be per-layer, not per-entity

  • Discussion: Current per-entity approach provides more flexibility

@NthTensor: Fold sorting into Transform2d

  • Agreement: Proposed unified approach addresses this

Consensus Points

  • Strong support for improved 2D ergonomics
  • Agreement both PRs provide more value together
  • Consensus that current Transform2d without sorting is insufficient
  • Shared concern about performance and complexity

Testing & Validation Requirements

Critical Test Scenarios

Sort Order Validation

// Test integrated depth system
spawn_entity(Transform2d { depth: Depth2d::Absolute(10.0), ..default() });
spawn_entity(Transform2d { depth: Depth2d::Relative(5.0), ..default() }); 
// Verify consistent, expected ordering

Hierarchy Propagation

// Test depth inheritance
spawn_parent(Transform2d { depth: Depth2d::Absolute(5.0), ..default() }, [
    spawn_child(Transform2d { depth: Depth2d::Relative(2.0), ..default() }), // Depth 7.0
    spawn_child(Transform2d::default()), // Depth 6.0 (5.0 + 1.0 default)
]);

Performance Benchmarks

  • Entity count scaling (1K, 10K, 100K entities)
  • Frame time impact measurement
  • Memory usage comparison vs current system

Acceptance Criteria

  1. Functional: All existing 2D examples work without modification
  2. Performance: <5% overhead compared to current Transform system
  3. Ergonomic: No 3D parent workarounds required for common 2D scenarios
  4. Compatible: Clear migration path for existing codebases

Conclusion & Next Steps

Recommendation: Proceed with Integrated Approach

The proposed unified Transform2d with integrated depth and sorting provides the best balance of:

  • Ergonomics: Single component for all 2D transform needs
  • Performance: Eliminates separate component overhead
  • Flexibility: Supports both relative and absolute depth control
  • Consistency: Maintains transform propagation semantics

Immediate Actions Required

  1. Resolve UI ZIndex naming conflict - Critical blocker
  2. Implement integrated Transform2d - Combines both PR benefits
  3. Add hierarchy propagation - Community expectation
  4. Performance validation - Ensure acceptable overhead

Long-term Vision

These changes establish foundation for advanced 2D features:

  • Sophisticated sorting strategies (custom sort keys)
  • 2D-specific optimization passes
  • Integrated 2D animation systems
  • Advanced 2D physics integration

The initial complexity investment is justified by the significant improvement to Bevy's 2D development experience and the foundation it provides for future 2D enhancements.

Select a repo