Files
reliquary/docs/workpad/research/bevy-skill-research/06-camera-gotchas.md
T
g4borg 325e278af1 Scaffold bevy plugin with bevy-0.19 and bevy-upgrade skills
Add plugin skeleton with versioned skill directories, TODO SKILL.md
scaffolds with goals for skill-creator, and copy over bevy-skill-research
reports from spacecraft into docs/workpad/research/.
2026-07-07 17:52:00 +02:00

97 lines
5.1 KiB
Markdown

# Bevy Skill Research: Camera, Rendering & Common Gotchas
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## Camera Setup
**Camera3d** is a marker component with `#[require(...)]` that auto-inserts: `Camera`, `DebandDither`, `CameraRenderGraph`, `Projection`, `Tonemapping`, `ColorGrading`, `Exposure`. Just spawning `Camera3d` gives a working 3D camera.
Coordinate system: right-handed, X-right, Y-up, Z-back (forward = -Z).
**0.18 change**: `RenderTarget` moved from a `Camera` field to a separate required component.
**0.19 change**: `Hdr` moved from `bevy_render` to `bevy_camera`; `screen_space_specular_transmission_*` fields extracted into `ScreenSpaceTransmission` component.
## Camera Ordering / Multiple Cameras
- `Camera.order` (isize) controls render order. Higher values render later (on top)
- Each camera with `is_active: true` renders independently
- `ClearColorConfig` controls whether a camera clears the previous output
- For overlays: use `ClearColorConfig::None` on higher-order cameras
## Camera Gotchas
- Default `far` plane is 1000 units — objects beyond are culled silently
- Forward is **-Z**, not +Z
- Viewport coordinates are in physical pixels, not logical
- GLTF models use +Z as visual forward (opposite Bevy convention)
## Coordinate System
| Axis | Bevy native | GLTF models |
|------|-------------|-------------|
| Forward | -Z (`Transform::forward()`) | +Z (`Transform::back()`) |
| Up | +Y | +Y |
| Right | +X | -X (from camera perspective) |
**GLTF orientation fix**: Use `look_to(-velocity_dir, Vec3::Y)` (negate direction) or read `transform.back()` for the model's visual forward.
## Rendering / Mesh / Material Gotchas
- **Mesh requires normals** for `StandardMaterial`. Missing normals = invisible mesh, no error
- **GLTF scenes** must be spawned with scene labels (`"model.gltf#Scene0"`), not the raw asset handle
- **Back-face culling** is on by default; incorrect vertex winding = invisible faces
- **Visibility hierarchy**: parent entities need visibility components or children are invisible
- **0.18**: Mesh access methods became `try_*` returning `Result<_, MeshAccessError>`. `MaterialPlugin` fields (`prepass_enabled`, `shadows_enabled`) became `Material` trait methods
- **0.19**: GLTF material loading returns `GltfMaterial` (use `#Material0/std` suffix for `StandardMaterial`). `Assets::get_mut` returns `AssetMut<A>` with mutation tracking. `Scene`/`SceneRoot` renamed to `WorldAsset`/`WorldAssetRoot`
## Transform Gotchas
- Bevy: +Y up, -Z forward, right-handed
- `Transform::forward()` returns -Z, `Transform::back()` returns +Z
- Transform propagation runs in **PostUpdate** — reading `GlobalTransform` in `Update` gives stale (last-frame) values
- **0.18**: `GltfPlugin` coordinate conversion restructured — `use_model_forward_direction` replaced by `convert_coordinates` with `rotate_scene_entity` and `rotate_meshes` flags
## Asset Loading Gotchas
- Assets are NOT ready immediately after `asset_server.load()`. Must check `AssetEvent::LoadedWithDependencies` or use `AssetServer::is_loaded_with_dependencies()`
- **0.18**: `LoadContext::path()` returns `AssetPath` not `Path`. Image `reinterpret_*` methods return `Result`
- **0.19**: `AssetPath::resolve()`/`resolve_embed()` take `&AssetPath` not `&str`; string variants renamed to `resolve_str()`/`resolve_embed_str()`
## Lighting Gotcha
**`AmbientLight` split (0.18)**:
- `GlobalAmbientLight` — resource, affects everything
- `AmbientLight` — camera component, per-camera ambient
## General LLM Traps
These are patterns where LLMs most commonly produce incorrect code:
| Trap | Wrong | Correct |
|------|-------|---------|
| Parent component | `Parent` | `ChildOf(entity)` (0.16+) |
| Child iteration | `children.iter().copied()` | `children.into_iter()` |
| Event writer | `EventWriter<T>` | `MessageWriter<T>` (0.18+) |
| Event reader | `EventReader<T>` | `MessageReader<T>` (0.18+) |
| Run condition | `on_event::<T>()` | `on_message::<T>()` (0.18+) |
| Despawn recursive | `despawn_recursive()` | `despawn()` (auto-recursive 0.16+) |
| Bundles | `#[derive(Bundle)]` | Tuples + `#[require(...)]` (0.16+) |
| WinitPlugin | `WinitPlugin::<WakeUp>` | `WinitPlugin` (not generic 0.18+) |
| GltfPlugin | `init_asset::<Gltf>()` | `GltfPlugin::default()` (0.18+) |
| Material bind group | `@group(2)` in shader | `@group(3)` (group 2 is mesh in 0.18) |
| System error handling | Return nothing | `-> Result` (0.18+) |
| GlobalTransform timing | Read in Update | Read in PostUpdate or accept lag |
| Same-state transition | `state.set(current)` no-ops | Triggers OnExit/OnEnter in 0.18; use `set_if_neq()` |
### 0.19-Specific Traps
| Trap | Detail |
|------|--------|
| Resources as components | `Query<Entity>` now matches resource entities — filter with `Without<IsResource>` |
| Scene renamed | `Scene`/`SceneRoot``WorldAsset`/`WorldAssetRoot` |
| Ref clone | `Ref<T>.clone()` returns `Ref<T>`, not inner value. Use `ref.deref().clone()` |
| Assets::get_mut | Returns `AssetMut<A>` with mutation tracking, not `&mut A` directly |
| GLTF materials | Returns `GltfMaterial`, use `#Material0/std` suffix for `StandardMaterial` |