No description
  • Rust 96.2%
  • GLSL 3.5%
  • Shell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-13 04:00:55 +01:00
demo better skybox 2026-08-13 04:00:55 +01:00
demo_basic better skybox 2026-08-13 04:00:55 +01:00
demo_xr better skybox 2026-08-13 04:00:55 +01:00
engine better skybox 2026-08-13 04:00:55 +01:00
engine_macros api cleanup, remove hecs, consistency and bug fixes 2026-07-20 05:05:50 +01:00
pak Add model instances, skinned animation, canvas UI, and pak packaging, etc 2026-07-15 20:45:11 +01:00
vertex_attributes_derive Add model instances, skinned animation, canvas UI, and pak packaging, etc 2026-07-15 20:45:11 +01:00
.gitconfig Create .gitconfig 2025-12-01 23:53:49 +00:00
.gitignore Add model instances, skinned animation, canvas UI, and pak packaging, etc 2026-07-15 20:45:11 +01:00
build.sh Add model instances, skinned animation, canvas UI, and pak packaging, etc 2026-07-15 20:45:11 +01:00
Cargo.lock api cleanup, remove hecs, consistency and bug fixes 2026-07-20 05:05:50 +01:00
Cargo.toml Refactor EC onto components; add XR and post stack 2026-07-18 23:40:17 +01:00
README.md fix reflection probes and other cleanup 2026-07-20 08:57:11 +01:00

BracketEngine 3

OpenGL editorless engine with entity component implementation and rapier physics

Setup

Requirements

  • Rust via rustup
  • C/C++ toolchain + pkg-config (GLFW, Assimp)
  • OpenGL 4.6 capable GPU

Linux packages

Arch:

sudo pacman -S base-devel pkgconf glfw-x11 assimp mesa

Debian/Ubuntu:

sudo apt install build-essential pkg-config libglfw3-dev libassimp-dev libgl1-mesa-dev

Build

git clone https://gitlab.bracketproto.com/BracketProto/BracketEngine3.git
cd BracketEngine3
cargo build --release -p bracket_engine
cargo run --release --bin <package>

Packaged builds

build.sh produces a release binary plus assets.pak (engine assets merged with the package assets/) under builds/<target>/<package>/.

./build.sh <package>           # linux + windows
./build.sh <package> linux     # x86_64-unknown-linux-gnu
./build.sh <package> windows   # x86_64-pc-windows-gnu (MinGW)

Architecture

run_app owns the window and frame loop and drives an EngineApp implementor. The runtime hub is Engine:

  • ec: ECManager (entities, components, behaviours, draw)
  • physics: PhysicsManager (Deref to PhysicsWorld, pose interpolation)
  • sound: SoundManager (rodio)
  • environment: SceneEnvironment (ambient, clear color, fog)
  • xr: XrManager (OpenXR session / tracking; also engine.start_xr())

Entity is a generational handle. Prefer ECManager (create_entity, add_component, hierarchy). Use ec.store() / ec.store_mut() (EntityStore) for bulk Transform/EntityInfo access and offline iteration.

Asset paths use utils::asset_path: resolve against assets.pak first, then loose assets/ directories.

Public types are re-exported from bracket_engine and bracket_engine::ec.

Frame loop

Per frame, in order:

  1. Awake / Start (awake on attach; start on first enable)
  2. FixedUpdate (Behaviour::fixed_update, physics step, rigid-body write-back)
  3. Update
  4. LateUpdate
  5. Render (scene resolve / lights, then Behaviour::render)
  6. OnGUI
  • Transform / EntityInfo are engine-owned fields on every entity (not Behaviours)
  • Other built-ins are Behaviours (#[derive(Behaviour)]); data-only types use an empty impl Behaviour
  • Storage is a per-entity component list (not ECS archetypes)
  • create_entity always adds Transform + EntityInfo; default names are Entity, Entity (1), ...

OnDestroy runs on entity removal and shutdown. Removals via remove_entity / remove_self are deferred to the end of the current update phase (fixed_update / update / late_update / on_gui) so mid-callback despawn does not break behaviour iteration. Pending entities are treated as gone for entity_alive, finds, and further behaviour callbacks. Hierarchy destroy runs children before the parent. Dynamic/kinematic body transforms are interpolated for rendering between fixed steps using Time::fixed_alpha. Disable with engine.physics.set_interpolation_enabled(false).

Entities and components

  • Hierarchy: set_parent, parent/child helpers on Entity, world TRS via ECManager
  • Activation: set_active for a subtree; behaviours also have per-Behaviour enable
  • Spawn: create_entity (Transform + EntityInfo; default name Entity / Entity (N))
  • Components (on ECManager and BehaviourContext for the current entity):
    • add_component / remove_component / remove_component_at
    • get_component / get_component_mut (first instance; returns Option)
    • get_components / get_components_mut (all of type via callback &[C] / &mut [C])
    • get_component_in_children / get_components_in_children / get_component_in_parent / get_components_in_parent (include_inactive: bool)
    • has_component / component_count
    • Multi-instance by default unless #[disallow_multiple_component]
  • Behaviours: #[derive(Behaviour)] (+ optional #[disallow_multiple_component], #[require_component(T, ...)]), implement Behaviour (extends Component), attach with add_component
  • Low-level: ec.store() / ec.store_mut() for iteration and direct Transform/EntityInfo access
  • BehaviourContext mirrors the component helpers for the current entity, plus environment, sound, physics / physics_mut, move_character, spawn_child, remove_self, set_enabled
  • Identity on EntityInfo: name, tag (default tags::UNTAGGED), layer (default layers::DEFAULT)
  • Lookup: find / find_with_tag / find_with_layer and plurals; find_first / find_all with EntityLookup; entities_with for a component type
  • Scope helpers: HierarchyScope with get_component_in_scope / for_each_components_in_scope

Every entity has Transform and EntityInfo.

Layers

Layers are free-form strings. The only builtin name is layers::DEFAULT (registry bit 0). Other names get a bit on first use (up to 32).

  • Membership: EntityInfo.layer / set_entity_layer. Used for render culling and Rapier collision membership.
  • Collision matrix: global, symmetric. set_layer_collision(a, b, collide) / get_layer_collision. Default is all pairs collide. ec.set_layer_collision updates the matrix and resyncs spawned RigidBody groups.
  • Render culling: Camera.culling_mask (LayerMask) selects which membership bits a camera draws. Independent of the collision matrix.
  • Bodies take groups from interaction_groups_for_layer (membership bit + matrix row). Optional RigidBody.collision_groups(...) overrides at spawn.

Rendering

  • Cameras: Camera::perspective / Camera::orthographic; multiple per entity; debug: true for free-fly; present via ec.prepare_render() / Camera::present_window / present_entities / present_view / rendering::frame::end (post from volumes); views via Camera::view_for_entity / view_for_entity_at
  • Camera clear settings are per camera: clear_color (Vec4), clear_color_enabled, clear_depth_enabled
  • Custom draws: Behaviour::render + MeshRenderer::enqueue / SkinnedMeshRenderer::enqueue / Skybox::enqueue, or rendering::queue::add_*
  • Render targets: RenderTexture / RenderTextureParams; assign with .target(...) or camera.target. Sample via rt.color(0) on materials, or rendering::queue::draw_to for low-level manual draws without post/UI
  • Lights: Light (directional/point/spot). Directional light feeds skybox sun params when present
  • SceneEnvironment: ambient color, clear color, fog near/far/color
  • Geometry: MeshRenderer, SkinnedMeshRenderer; ModelInstance::new(path) builds the tree in awake
  • Reflections: ReflectionProbe (OnStart / Realtime / custom cubemap / recapture; capture runs in Behaviour::post_render after the render queue is built)
  • Sky: Skybox (procedural default, or custom shader/material)
  • Materials: StandardMaterial (PBR) or Material + Shader. Built-in GLSL and named SSBO bindings under engine/assets/shaders and resources/shaders
  • Primitives: PrimitiveBuilder unit meshes; scale with Transform
  • Immediate overlay draw (no entities): Sprite, BitmapFont
  • Post: PostVolume + PostEffect (Bloom, Tonemap, custom); Camera.post (Auto / On / Off); writes to Camera.target or the bound framebuffer (window / XR eye)

Physics

Rapier3D behind RigidBody + ColliderDesc. Collision/trigger callbacks land on Behaviour as ColliderHit (on_trigger_* / on_collision_*).

Colliders

  • Primitives take full sizes: cube(sx,sy,sz), sphere(diameter), capsule(height, diameter), cylinder(height, diameter). Same convention as scaled unit meshes from PrimitiveBuilder.
  • From renderers: trimesh_from_renderers() (static), convex_from_renderers() (dynamics), aabb_from_renderers()
  • Friction / restitution live on the collider (also settable via RigidBody builders at spawn)

Bodies

  • Kinds: static / kinematic / dynamic
  • Common knobs: gravity_scale, lock_rotations, damping, CCD, sleep
  • Forces: apply_force / apply_impulse (and point/torque variants)
  • Kinematic: apply_kinematic_pose (pose + next target + transform), set_kinematic_target (next step only)
  • Visual interpolation between fixed steps (see frame loop)

Queries (PhysicsManager / PhysicsWorld)

  • raycast / raycast_filtered
  • Shape casts / overlaps: sphere, cube, capsule (full sizes like ColliderDesc)
  • Raw: query_pipeline / query_pipeline_mut

Character controller

  • Capsule kinematic mover. Adds a locked kinematic RigidBody if the entity has none.
  • High level: ctx.move_character or CharacterController::move_entity
  • Low level: move_by(motion, dt, filter) absolute displacement for the step (no built-in gravity/jump)
  • Result: CharacterMoveResult (flags, grounded, velocity, delta); CollisionFlags (NONE, SIDES, ABOVE, BELOW)

Joints: Joint + JointDesc (fixed, revolute, spherical, prismatic, rope, spring; local_anchor1 / local_anchor2, contacts_enabled).

Animation

  • Load with ModelInstance::new(path) on an entity (builds the node/mesh tree)
  • Skinned: Animator + SkinnedMeshRenderer; static meshes use MeshRenderer
  • Animator::play / cross_fade / blend shapes / find_node
  • play_at takes ticks (clip.duration()). Wall-clock length is clip.length_seconds()

Audio

engine.sound (SoundManager): load/play 2D clips. Scene audio uses AudioSource on entities and a single AudioListener (usually on the camera).

UI

  • Retained: Canvas, RectTransform, Image / Text (or Drawable for custom UiDrawable)
  • Layout resolves during ec.prepare_render() before present
  • Immediate: Sprite / BitmapFont
  • imgui via gui::with_ui

Profiler: profiler::set_enabled / profiler::scope (hierarchy filter + selection gizmo when open).

Window, time, input

  • Window: DisplayMode, PresentMode, RenderResolution; optional manual present via ec.prepare_render() / Camera::present_* / rendering::frame::end + swap_buffers
  • Time: delta_time(), time_scale() / set_time_scale(), fixed_delta_time(), fixed_alpha()
  • Input: keyboard / mouse / gamepad, plus named axis / action bindings under bracket_engine::input
  • Host stats: System::resources() for CPU/RAM; GL vendor/renderer/version after context init
  • XR: XrManager on Engine, XrOrigin.tracking_origin, Camera.stereo_target_eye + mirror_blit_mode / .xr(), Engine::start_xr(). OpenXR loader required at runtime.

Source map

  • engine/src/app.rs: EngineApp, run_app
  • engine/src/engine.rs: Engine
  • engine/src/window.rs, time.rs, input/: display, clock, input
  • engine/src/system: SystemResources
  • engine/src/ec/ec_manager.rs: ECManager
  • engine/src/ec/component.rs: Component, Behaviour
  • engine/src/ec/behaviour_context.rs: BehaviourContext
  • engine/src/ec/entity.rs: Entity
  • engine/src/ec/entity_store.rs: EntityStore (low-level)
  • engine/src/ec/layer.rs: LayerMask, layer registry, collision matrix
  • engine/src/ec/components/: built-in components (Transform, renderers, physics, audio, UI, XR, ...)
  • engine/src/ec/collision_events.rs: collision/trigger event dispatch for Behaviours
  • engine/src/physics/: PhysicsManager, PhysicsWorld, ColliderDesc, hits
  • engine/src/rendering/: environment, queue, sprites, probes, post
  • engine/src/resources/: meshes, materials, shaders, primitives, models
  • engine/src/audio/: SoundManager
  • engine/src/xr/: OpenXR session, stereo frame, tracking
  • engine_macros/: #[derive(Behaviour)] (re-exported; also emits storage EngineComponent impl)

Examples

Starter app

use bracket_engine::ec::{
    AudioListener, Camera, Light, ModelInstance, ReflectionProbe, Skybox,
};
use bracket_engine::{run_app, window, Engine, EngineApp};
use nalgebra_glm as glm;

struct Game;

impl EngineApp for Game {
    fn start(&mut self, engine: &mut Engine) {
        window::set_display_mode(window::DisplayMode::Windowed);
        window::set_vsync(true);

        engine.environment.ambient_color = glm::vec3(0.05, 0.05, 0.08);
        engine.environment.fog_far = 100.0;

        let ec = &mut engine.ec;
        let sky = ec.create_entity();
        ec.add_component(sky, Skybox::new());

        let camera = ec.create_entity();
        ec.transform_mut(camera)
            .unwrap()
            .set_position(glm::vec3(0.0, 1.5, 4.0));
        let mut cam = Camera::perspective(90.0, 0.01, 1000.0);
        cam.debug = true;
        ec.add_component(camera, cam);
        ec.add_component(camera, AudioListener);

        let scene = ec.create_entity();
        ec.add_component(scene, ModelInstance::new("path/to/model.gltf"));

        let light = ec.create_entity();
        ec.transform_mut(light)
            .unwrap()
            .set_position(glm::vec3(2.0, 3.0, 1.0));
        ec.add_component(
            light,
            Light::point(glm::vec3(1.0, 0.9, 0.8), 5.0, 20.0),
        );

        let probe = ec.create_entity();
        ec.transform_mut(probe)
            .unwrap()
            .set_position(glm::vec3(0.0, 2.0, 0.0));
        ec.add_component(
            probe,
            ReflectionProbe::on_start(128)
                .box_size(glm::vec3(40.0, 20.0, 40.0))
                .blend_distance(4.0)
                .intensity(1.0),
        );
    }
}

fn main() {
    run_app(Game, "My Game", 1280, 720);
}

Physics and triggers

use bracket_engine::Engine;
use bracket_engine::ec::{
    Behaviour, ColliderHit, ECManager, ModelInstance, RigidBody, BehaviourContext,
};
use bracket_engine::Behaviour;
use bracket_engine::physics::{ColliderDesc, QueryFilter};

let ec = &mut Engine::current().ec;

let ground = ec.create_entity();
ec.add_component(ground, ModelInstance::new("path/to/ground.gltf"));
ec.add_component(
    ground,
    RigidBody::static_body(ColliderDesc::trimesh_from_renderers()).friction(0.5),
);

let ball = ec.create_entity();
ec.transform_mut(ball)
    .unwrap()
    .set_position(glm::vec3(0.0, 5.0, 0.0));
ec.add_component(
    ball,
    RigidBody::dynamic(ColliderDesc::sphere(1.0).friction(0.1)),
);

#[derive(Behaviour)]
struct Volume;
impl Behaviour for Volume {
    fn on_trigger_enter(&mut self, _ctx: &mut BehaviourContext<'_>, hit: ColliderHit) {
        let _ = hit.other;
    }
}

let volume = ec.create_entity();
ec.add_component(
    volume,
    RigidBody::static_body(ColliderDesc::sphere(2.0).sensor(true)),
);
ec.add_component(volume, Volume);

let hit = Engine::current().physics.raycast_filtered(
    glm::vec3(0.0, 10.0, 0.0),
    glm::vec3(0.0, -1.0, 0.0),
    100.0,
    QueryFilter::default(),
);
let _ = hit;

Character movement

use bracket_engine::ec::{CharacterController, CollisionFlags};
use bracket_engine::physics::QueryFilter;

ec.set_layer_collision("Player", "Ball", false);

let player = ec.create_entity();
ec.set_entity_layer(player, "Player");
ec.add_component(
    player,
    CharacterController::new()
        .height(1.7)
        .radius(0.3)
        .slope_limit(50.0),
);

// In Behaviour::update; move uses body groups from layer + collision matrix:
let Some(moved) = ctx.move_character(velocity * dt, dt, QueryFilter::default()) else {
    return;
};
if moved.grounded {
    velocity.y = velocity.y.min(0.0);
}
if moved.flags.intersects(CollisionFlags::ABOVE) {
    velocity.y = velocity.y.min(0.0);
}
let _delta = moved.delta;
// Outside behaviours: CharacterController::move_entity(entity, motion, dt, filter)

Primitives

use std::rc::Rc;
use bracket_engine::ec::{MeshRenderer, RigidBody};
use bracket_engine::physics::ColliderDesc;
use bracket_engine::resources::{PrimitiveBuilder, StandardMaterial};

let mesh = Rc::new(PrimitiveBuilder::cube().build());
let mat = StandardMaterial::new();
let entity = ec.create_entity();
ec.transform_mut(entity)
    .unwrap()
    .set_scale(glm::vec3(1.0, 1.0, 1.0));
ec.add_component(entity, MeshRenderer::mesh(mesh, mat));
ec.add_component(
    entity,
    RigidBody::static_body(ColliderDesc::cube(1.0, 1.0, 1.0)),
);

Animation

use bracket_engine::Engine;
use bracket_engine::ec::{Animator, ECManager, ModelInstance};

let ec = &mut Engine::current().ec;
let entity = ec.create_entity();
ec.add_component(entity, ModelInstance::new("character.fbx"));
ec.transform_mut(entity)
    .unwrap()
    .set_scale(glm::vec3(0.01, 0.01, 0.01));

if let Some(mut animator) = ec.get_component_mut::<Animator>(entity) {
    animator.play("Idle");
    animator.play_at("Idle", 30.0);
    animator.set_speed(1.0);
    animator.set_normalized_time(0.0);
    animator.cross_fade("Walk", 0.25);
    let _hand = animator.find_node("Hand_R");
    animator.set_blend_shape_weight("Smile", 1.0);
}

Materials and shaders

Material / StandardMaterial for PBR. Custom shaders: Shader + Material::new. Includes and buffer names live under engine/assets/shaders and resources/shaders.

Custom shader:

use std::{cell::RefCell, rc::Rc};
use bracket_engine::{
    rendering::RenderType,
    resources::{shaders::Shader, Material},
    utils,
};

let vert = utils::asset_path("shaders/required/standard/standard.vert");
let frag = utils::asset_path("shaders/required/standard/standard.frag");
let shader = Rc::new(RefCell::new(Shader::new(&vert, &frag)));
let mut material = Material::new(shader, RenderType::Opaque as u32);
material.set_vec4("AlbedoColor", &glm::vec4(1.0, 1.0, 1.0, 1.0));

Custom SSBO binding:

layout(std430, binding = MyBindingName) readonly buffer MyBindingName { mat4 items[]; };
use bracket_engine::resources::shaders::{binding_manager, Ssbo};

let binding = binding_manager::binding("MyBindingName");
if binding >= 0 {
    ssbo.update(&data, gl::DYNAMIC_DRAW);
    ssbo.bind(binding as u32);
}

Parenting, tags, layers

use bracket_engine::Engine;
use bracket_engine::ec::{layers, ECManager, LayerMask};

let ec = &mut Engine::current().ec;
let parent = ec.create_entity();
let child = ec.create_entity();
ec.set_parent(child, Some(parent));
ec.set_entity_name(child, "Child");
ec.set_entity_tag(child, "Enemy");
ec.set_entity_layer(child, layers::DEFAULT);
ec.set_layer_collision("Enemy", "Player", false);

// camera.culling_mask = LayerMask::from_layer(layers::DEFAULT) | LayerMask::from_layer("Water");

let _ = ec.find("Child");
let _ = ec.find_with_tag("Enemy");

ec.set_parent(child, None);
ec.remove_entity(parent);

Overlay HUD

use bracket_engine::ec::{Canvas, RectTransform, Text};
use bracket_engine::rendering::FontAsset;
use bracket_engine::utils;

let font = FontAsset::load(&utils::asset_path(
    "shaders/required/font/monospace.png",
));
let canvas = ec.create_entity();
ec.add_component(canvas, RectTransform::fullscreen());
ec.add_component(
    canvas,
    Canvas::overlay()
        .reference(1920.0, 1080.0)
        .match_width_or_height(0.5),
);
let label = ec.create_entity();
ec.add_component(
    label,
    RectTransform::top_left(glm::vec2(16.0, -16.0), glm::vec2(600.0, 48.0)),
);
ec.add_component(label, Text::new("Hello", font).font_size(1.0));
ec.set_parent(label, Some(canvas));

Audio

use bracket_engine::Engine;
use bracket_engine::ec::{AudioSource, ECManager};

let sound = &mut Engine::current().sound;
sound.load_sound("sfx/click.wav");
sound.play_2d_sound("sfx/click.wav", 1.0, 0.0);

let ec = &mut Engine::current().ec;
let source = ec.create_entity();
ec.add_component(
    source,
    AudioSource::new("sfx/footstep.wav")
        .spatial(25.0)
        .play_on_start(true),
);

Custom Behaviour

use bracket_engine::ec::{Behaviour, BehaviourContext};
use bracket_engine::Behaviour;

#[derive(Behaviour)]
struct Spinner {
    speed: f32,
}

impl Behaviour for Spinner {
    fn update(&mut self, ctx: &mut BehaviourContext<'_>, dt: f32) {
        let Some(mut transform) = ctx.transform_mut() else {
            return;
        };
        let yaw = glm::quat_angle_axis(self.speed * dt, &glm::vec3(0.0, 1.0, 0.0));
        transform.set_rotation(yaw * transform.rotation());
    }
}

let ec = &mut Engine::current().ec;
let entity = ec.create_entity();
ec.add_component(entity, Spinner {
    speed: 90.0_f32.to_radians(),
});

Input and OnGUI

use bracket_engine::input;

let mut velocity = glm::vec3(0.0, 0.0, 0.0);
velocity.x += input::axis("Horizontal");
velocity.z += input::axis("Vertical");
if input::action_pressed("Jump") {
    // jump
}

input::register_action(
    input::ActionBinding::new("Interact")
        .with_key(input::Key::E)
        .with_gamepad_button(input::GamepadButton::ButtonX),
);

Device-level helpers remain (key_down, gamepad_stick, button_down, etc.).

use bracket_engine::ec::{Behaviour, BehaviourContext};
use bracket_engine::Behaviour;

#[derive(Behaviour)]
struct MyPanel {
    speed: f32,
}

impl Behaviour for MyPanel {
    fn on_gui(&mut self, _ctx: &mut BehaviourContext<'_>) {
        bracket_engine::gui::with_ui(|ui| {
            ui.window("Debug").build(|| {
                ui.slider("Speed", 0.0, 10.0, &mut self.speed);
            });
        });
    }
}

System resources

use bracket_engine::System;

let stats = System::resources();
if stats.cpu_ready {
    let _cpu = stats.cpu_usage; // 0..=100
}
if stats.ram_ready {
    let _ram_mb = stats.ram_used_mb;
    let _ram_total_mb = stats.ram_total_mb;
}
let _gpu = (&stats.gpu_vendor, &stats.gpu_renderer, &stats.gpu_version);

Custom present

use bracket_engine::Engine;
use bracket_engine::ec::Camera;
use bracket_engine::window::{self, PresentMode};

window::set_present_mode(PresentMode::Manual);

let ec = &mut Engine::current().ec;
let other_camera = ec.create_entity();
ec.transform_mut(other_camera)
    .unwrap()
    .set_position(glm::vec3(4.0, 1.5, 0.0));
ec.add_component(other_camera, Camera::perspective(60.0, 0.01, 1000.0));

let primary = Camera::primary_entity(ec.store());
let cameras: Vec<_> = primary
    .into_iter()
    .chain(std::iter::once(other_camera))
    .collect();

ec.prepare_render();
Camera::present_entities(ec, &cameras);
bracket_engine::rendering::frame::end();
window::swap_buffers();

Post-processing

use bracket_engine::ec::{Camera, PostMode, PostVolume};
use bracket_engine::rendering::{Bloom, PostEffect, PostEffectContext, Tonemap};

ec.add_component(
    ec.create_entity(),
    PostVolume::global()
        .effect(Bloom::default().threshold(1.0).intensity(0.6).levels(5))
        .effect(Tonemap::aces()),
);

ec.add_component(
    ec.create_entity(),
    PostVolume::local(glm::vec3(20.0, 10.0, 20.0))
        .blend_distance(4.0)
        .priority(10)
        .effect(Bloom::default().intensity(0.9).levels(5)),
);

let cam = Camera::perspective(60.0, 0.01, 1000.0)
    .post(PostMode::Auto);

struct Grayscale;
impl PostEffect for Grayscale {
    fn name(&self) -> &str {
        "Grayscale"
    }
    fn apply(&mut self, ctx: &mut PostEffectContext<'_>) {
        let _ = ctx;
    }
}

Volumes resolve by priority and local box blend. Bloom knobs: threshold, soft_knee, intensity, levels (2-8), scatter, clamp.

RenderTexture

use std::rc::Rc;

use bracket_engine::ec::Camera;
use bracket_engine::rendering::{RenderTexture, RenderTextureParams};
use bracket_engine::resources::{ColorFormat, DepthFormat};

let rt = RenderTexture::shared(512, 512);

let mut cam = Camera::perspective(60.0, 0.01, 1000.0).target(Rc::clone(&rt));
cam.priority = 10;
ec.add_component(portal_camera, cam);

material.set_texture("Albedo", rt.borrow().color(0));

let hdr = RenderTexture::with_params(
    RenderTextureParams::new(1024, 1024)
        .with_color_format(ColorFormat::RGBA16F)
        .with_depth_format(Some(DepthFormat::Depth32FStencil8))
        .with_mipmaps(true),
);

ec.prepare_render();
if let Some(view) = Camera::view_for_entity(ec.store(), portal_camera) {
    bracket_engine::rendering::queue::draw_to(ec.store(), &hdr, &view, true);
}
bracket_engine::rendering::frame::end();

XR

OpenXR loader required at runtime. Create an XrOrigin (with tracking_origin) before starting. engine.start_xr() reads tracking from that component; otherwise use set_tracking_origin. start() / stop() can run at any time: the session waits until views are valid (is_running()) or times out. stop() or session loss restores the previous present mode. Also is_starting() / last_error().

.xr() sets stereo_target_eye to Both and mirror_blit_mode to LeftEye (desktop preview blits the left eye buffer). Override with .stereo_target_eye(...) / .mirror_blit_mode(...). A second window camera (StereoTargetEye::None) is a separate scene render. Window cameras still go through the normal present stack (priority, clear settings, targets, post). Mirror blit does not draw UI.

use bracket_engine::ec::{Camera, PostMode, XrController, XrHand, XrOrigin};
use bracket_engine::{Engine, TrackingOrigin};

let origin = ec.create_entity();
ec.add_component(
    origin,
    XrOrigin::new().tracking_origin(TrackingOrigin::Floor),
);

let head = ec.create_entity();
ec.set_parent(head, Some(origin));
ec.add_component(
    head,
    Camera::perspective(90.0, 0.01, 1000.0)
        .xr()
        .post(PostMode::Off),
);

engine.start_xr().expect("OpenXR session");

let left = ec.create_entity();
ec.set_parent(left, Some(origin));
ec.add_component(left, XrController::left()); // or XrController::new(XrHand::Left)

start() / start_xr() suggest common interaction profiles (khr/simple_controller plus microsoft/htc/oculus/valve). Override with suggest_bindings before start.