Bevy Forge 0.1.0: visual scene editor for Bevy 0.20 BSN
This commit is contained in:
+569
@@ -0,0 +1,569 @@
|
||||
# Scene format (`.forge.ron`)
|
||||
|
||||
Bevy Forge stores scenes as human-readable [RON](https://github.com/ron-rs/ron) documents with the
|
||||
`.forge.ron` extension. A scene file contains **editor data only** - entities, hierarchy, transforms
|
||||
and components. It never contains generated code and is not Bevy's `.scn.ron` dynamic-scene format.
|
||||
BSN or imperative Rust is generated from a scene on demand; see the [BSN export guide](bsn-export.md).
|
||||
|
||||
This page documents format version **1** as written by the editor and understood by the core crate.
|
||||
Related pages: [architecture](architecture.md), [README](../README.md).
|
||||
|
||||
## Files and naming
|
||||
|
||||
- Scenes live in `<project>/scenes/`. `Project::list_scenes()` lists the `*.forge.ron` files there.
|
||||
- The editor's suggested filename is the scene display name run through `slugify`: ASCII
|
||||
letters/digits are lowercased, whitespace and `-`, `_`, `.` collapse to `-`, and an empty result
|
||||
becomes `scene` (for example `Level 01` -> `level-01.forge.ron`). The name inside the file is
|
||||
separate and can be any UTF-8 string.
|
||||
- The editor refuses to save under any other extension, and `Project::save_scene` refuses paths
|
||||
outside the project root.
|
||||
- Files are UTF-8 RON produced with a two-space pretty printer. Writers use `\n` line endings.
|
||||
RON comments are legal in the format but the editor does not write them.
|
||||
- Whitespace and line breaks are not significant. The editor's writer puts one struct field per
|
||||
line and expands nested vectors/colours the same way; the examples on this page are written more
|
||||
compactly for readability and parse identically.
|
||||
|
||||
## Complete example
|
||||
|
||||
A small 3D scene with two roots, a directional light and a primitive mesh:
|
||||
|
||||
```ron
|
||||
(
|
||||
format_version: 1,
|
||||
name: "Garden",
|
||||
entities: {
|
||||
1: (
|
||||
id: 1,
|
||||
name: "Sun",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: -4.0, y: 8.0, z: -6.0),
|
||||
rotation: (x: 45.0, y: 30.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: directional_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||
illuminance: 10000.0,
|
||||
shadows: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
2: (
|
||||
id: 2,
|
||||
name: "Ground",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: mesh3d,
|
||||
data: (
|
||||
mesh: (
|
||||
source: primitive,
|
||||
value: cuboid(size: (x: 12.0, y: 0.2, z: 12.0)),
|
||||
),
|
||||
material: (
|
||||
base_color: (r: 0.35, g: 0.55, b: 0.3, a: 1.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.9,
|
||||
unlit: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
},
|
||||
roots: [1, 2],
|
||||
next_id: 3,
|
||||
)
|
||||
```
|
||||
|
||||
A 2D scene with hierarchy, a sprite, text, a camera, a hidden component and a custom component:
|
||||
|
||||
```ron
|
||||
(
|
||||
format_version: 1,
|
||||
name: "Platformer",
|
||||
entities: {
|
||||
1: (
|
||||
id: 1,
|
||||
name: "Camera",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 0.0, z: 10.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(type: camera2d),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
2: (
|
||||
id: 2,
|
||||
name: "Player",
|
||||
parent: None,
|
||||
children: [3],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 1.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 32.0, y: 48.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
),
|
||||
(type: visibility, data: visible),
|
||||
(
|
||||
type: custom,
|
||||
data: (
|
||||
type_path: "forge_demo::Orbit",
|
||||
fields: [
|
||||
(name: "radius", expr: "2.0"),
|
||||
(name: "speed", expr: "-0.5"),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
3: (
|
||||
id: 3,
|
||||
name: "Label",
|
||||
parent: Some(2),
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 32.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: text2d,
|
||||
data: (
|
||||
text: "Bevy Forge",
|
||||
font_size: 24.0,
|
||||
color: (r: 0.9, g: 0.9, b: 1.0, a: 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
},
|
||||
roots: [1, 2],
|
||||
next_id: 4,
|
||||
)
|
||||
```
|
||||
|
||||
## Top-level fields
|
||||
|
||||
`SceneDoc` is a RON struct with these fields, in this order:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `format_version` | `u32` | Schema version. Currently `1`; see [Versioning](#versioning). |
|
||||
| `name` | `String` | Scene display name, independent of the filename. May be empty (validation warns). |
|
||||
| `entities` | map `u64 -> EntityNode` | All entities, keyed by ID. Map order is insertion order and is stable across saves. |
|
||||
| `roots` | `[u64]` | Root entity IDs in display order. Every entity is either a root or a descendant of one. |
|
||||
| `next_id` | `u64` | Next identity to allocate. Must be greater than every ID in `entities`; IDs are never reused. |
|
||||
|
||||
## Entity fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `id` | `u64` | Must equal the key in `entities`. |
|
||||
| `name` | `String` | Display name; arbitrary UTF-8. Used for hierarchy labels and generated `Name(...)`. |
|
||||
| `parent` | `Option<u64>` | `None` for roots, otherwise the containing entity. |
|
||||
| `children` | `[u64]` | Ordered child IDs. Order is preserved in the hierarchy and in generated code. |
|
||||
| `transform` | `Transform` | Mandatory local transform; see below. |
|
||||
| `components` | `[ComponentData]` | Ordered component list; at most one component per `key()`. |
|
||||
| `locked` | `bool` | Editor-only. `true` blocks viewport transforms and gizmos, not hierarchy edits or deletion. Missing in old files defaults to `false`. |
|
||||
|
||||
## Transforms and coordinates
|
||||
|
||||
`Transform` mirrors Bevy's `Transform`:
|
||||
|
||||
```ron
|
||||
transform: (
|
||||
translation: (x: 1.0, y: 2.0, z: 0.5),
|
||||
rotation: (x: 0.0, y: 45.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
)
|
||||
```
|
||||
|
||||
- `translation` is the local position. 2D scenes use X/Y; Z is used as the draw-order axis by the
|
||||
2D viewport. 3D scenes use all three axes.
|
||||
- `rotation` is **XYZ Euler angles in degrees**, not radians and not a quaternion. This is the value
|
||||
the inspector edits. Export converts it with
|
||||
`Quat::from_euler(EulerRot::XYZ, x.to_radians(), y.to_radians(), z.to_radians())`.
|
||||
- `scale` is a per-axis multiplier. A zero scale is legal in the file but produces a validation
|
||||
warning.
|
||||
- World transforms are computed by composing local transforms down the parent chain. Parents affect
|
||||
children; moving a parent moves its subtree in the viewports. Scene files always store local
|
||||
transforms, never world transforms.
|
||||
- All numbers are `f32` RON floats. Non-finite values (`NaN`, `inf`) are representable in RON but
|
||||
rejected by validation and code generation.
|
||||
|
||||
Colours use the four-field `Color` struct with channels in `0.0..=1.0`:
|
||||
|
||||
```ron
|
||||
color: (r: 0.35, g: 0.55, b: 0.95, a: 1.0)
|
||||
```
|
||||
|
||||
The editor edits these as sRGB colours; the generated code emits `Color::srgba(r, g, b, a)`.
|
||||
|
||||
## Components
|
||||
|
||||
`components` is an ordered list of serde-tagged values. Every entry has a `type` tag and, except for
|
||||
unit variants, a `data` payload. Tags and enum payloads are **bare RON identifiers**, not strings -
|
||||
write `type: sprite`, not `type: "sprite"`:
|
||||
|
||||
```ron
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 32.0, y: 48.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
The tag values and uniqueness keys are:
|
||||
|
||||
| Tag | Payload | `key()` | Notes |
|
||||
|---|---|---|---|
|
||||
| `sprite` | `SpriteData` | `sprite` | 2D sprite |
|
||||
| `text2d` | `Text2dData` | `text2d` | world-space text |
|
||||
| `camera2d` | none | `camera2d` | unit: `(type: camera2d)` |
|
||||
| `camera3d` | none | `camera3d` | unit: `(type: camera3d)` |
|
||||
| `point_light` | `PointLightData` | `point_light` | |
|
||||
| `directional_light` | `DirectionalLightData` | `directional_light` | |
|
||||
| `mesh3d` | `Mesh3dData` | `mesh3d` | primitive or asset mesh plus material |
|
||||
| `visibility` | `VisibilityMode` | `visibility` | `inherited`, `visible` or `hidden` |
|
||||
| `custom` | `CustomComponent` | `custom:<type_path>` | one per custom type path |
|
||||
|
||||
`Name` and `Transform` are **not** components in this format - they are entity fields. Adding a
|
||||
component whose key already exists is refused; use the inspector's edit controls to change values.
|
||||
|
||||
### Sprite
|
||||
|
||||
```ron
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 64.0, y: 64.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `image` | `Option<String>` | Path relative to `assets/`; omit `Some` for a solid-colour sprite |
|
||||
| `color` | `Color` | Tint; defaults to white |
|
||||
| `custom_size` | `Option<Vec2>` | Explicit size; `None` uses the texture's native size |
|
||||
| `flip_x`, `flip_y` | `bool` | Mirror the texture on that axis |
|
||||
|
||||
A sprite with no image and no custom size renders nothing; validation reports a warning.
|
||||
|
||||
### Text2d
|
||||
|
||||
```ron
|
||||
(
|
||||
type: text2d,
|
||||
data: (
|
||||
text: "Hello\nBevy",
|
||||
font_size: 32.0,
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`font_size` is in pixels and must be greater than zero. The text string is escaped as a normal RON
|
||||
string and becomes a Rust string literal on export. Empty text is allowed but produces a warning.
|
||||
|
||||
### Cameras
|
||||
|
||||
```ron
|
||||
(type: camera2d)
|
||||
```
|
||||
|
||||
```ron
|
||||
(type: camera3d)
|
||||
```
|
||||
|
||||
A scene can contain both, but the 2D and 3D camera mix produces a warning. Optional cameras are
|
||||
reported as info; more than one camera is also info. Neither variant stores projection fields in
|
||||
this version.
|
||||
|
||||
### Point light
|
||||
|
||||
```ron
|
||||
(
|
||||
type: point_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.8, b: 0.6, a: 1.0),
|
||||
intensity: 800.0,
|
||||
range: 20.0,
|
||||
shadows: true,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`intensity` must be non-negative and `range` must be positive; `shadows` maps to
|
||||
`shadow_maps_enabled` in generated BSN.
|
||||
|
||||
### Directional light
|
||||
|
||||
```ron
|
||||
(
|
||||
type: directional_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||
illuminance: 10000.0,
|
||||
shadows: true,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`illuminance` must be non-negative. The light's direction comes from the entity's rotation.
|
||||
|
||||
### Mesh3d
|
||||
|
||||
```ron
|
||||
(
|
||||
type: mesh3d,
|
||||
data: (
|
||||
mesh: (
|
||||
source: primitive,
|
||||
value: cuboid(size: (x: 1.0, y: 1.0, z: 1.0)),
|
||||
),
|
||||
material: (
|
||||
base_color: (r: 0.45, g: 0.62, b: 0.95, a: 1.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.5,
|
||||
unlit: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`MeshSource` is tagged `source` / `value`:
|
||||
|
||||
- `source: primitive` with one of the five `PrimitiveShape` values below.
|
||||
- `source: asset` with a string mesh path, for example
|
||||
`value: "models/robot.gltf#Mesh0/Primitive0"`.
|
||||
|
||||
Primitive shapes (RON value for `value`):
|
||||
|
||||
| Shape | RON |
|
||||
|---|---|
|
||||
| Cuboid | `cuboid(size: (x: 2.0, y: 1.0, z: 0.5))` |
|
||||
| Sphere | `sphere(radius: 0.5)` |
|
||||
| Plane | `plane(size: (x: 10.0, y: 10.0))` - width and depth in X/Z |
|
||||
| Cylinder | `cylinder(radius: 0.5, height: 1.0)` |
|
||||
| Capsule | `capsule(radius: 0.5, length: 1.0)` |
|
||||
|
||||
`MaterialData` fields:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `base_color` | `Color` | Albedo / base colour |
|
||||
| `metallic` | `f32` | 0 = dielectric, 1 = metal; values outside `0..=1` warn |
|
||||
| `perceptual_roughness` | `f32` | 0 = mirror, 1 = rough; values outside `0..=1` warn |
|
||||
| `unlit` | `bool` | Emit an unlit material |
|
||||
|
||||
Asset mesh paths are validated for existence and kind when an asset index is available, and are
|
||||
emitted as `Mesh3d("<path>")` in BSN. The material is always emitted inline, so no material asset
|
||||
loader is required.
|
||||
|
||||
### Visibility
|
||||
|
||||
```ron
|
||||
(type: visibility, data: hidden)
|
||||
```
|
||||
|
||||
The payload is one of the bare RON identifiers `inherited`, `visible` or `hidden`. `hidden` is
|
||||
equivalent to Bevy's `Visibility::Hidden`; toggling the hierarchy eye adds/removes this component.
|
||||
An entity without a visibility component is treated as `inherited`.
|
||||
|
||||
### Custom components
|
||||
|
||||
```ron
|
||||
(
|
||||
type: custom,
|
||||
data: (
|
||||
type_path: "forge_demo::Orbit",
|
||||
fields: [
|
||||
(name: "radius", expr: "2.0"),
|
||||
(name: "speed", expr: "-0.5"),
|
||||
(name: "axis", expr: "Vec3::Y"),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `type_path` | `String` | A Rust type path such as `Health`, `my_game::Health` or `my_game::components::Health` |
|
||||
| `fields` | `[CustomField]` | Ordered field initialisers |
|
||||
| `fields[].name` | `String` | Rust field identifier |
|
||||
| `fields[].expr` | `String` | Raw Rust/BSN expression, stored verbatim and emitted after `name:` |
|
||||
|
||||
Custom components are the escape hatch for project-specific types. The editor validates only the
|
||||
shape of the type path and field names, and that expressions are non-empty; it does not parse Rust.
|
||||
The component type must derive `Component` plus `Default + Clone` or `FromTemplate` - see
|
||||
[custom-component requirements](bsn-export.md#custom-component-requirements). Two custom components
|
||||
with different type paths can coexist on one entity; the same path may appear only once.
|
||||
|
||||
## Ids, forests and ordering
|
||||
|
||||
Every scene is a forest of entities:
|
||||
|
||||
- Each entity has exactly one parent (`None` for roots) and appears in exactly one place: either
|
||||
`roots` or one parent's `children` list.
|
||||
- `parent` and `children` must agree (`child.parent == Some(parent_id)`). `roots` entries must have
|
||||
`parent: None`.
|
||||
- Cycles are invalid and are rejected by `reparent`; a hand-edited file with a cycle fails
|
||||
structural validation.
|
||||
- `id` is unique and stable for the lifetime of the scene; IDs are never reused after deletion.
|
||||
`next_id` must remain greater than every entity ID. Gaps are fine.
|
||||
- Order matters: `roots` controls root order, each `children` list controls sibling order, and both
|
||||
are preserved by duplicate/reparent and by code generation.
|
||||
- Deleting an entity removes its whole subtree. Duplicating deep-copies the subtree, inserts the
|
||||
copy immediately after the original among its siblings, and suffixes the root copy's name with
|
||||
`" Copy"`.
|
||||
- `locked` is editor metadata and has no effect on export.
|
||||
|
||||
`SceneDoc::check_integrity()` returns a list of structural problems (empty when valid). Validation
|
||||
includes these checks.
|
||||
|
||||
## Versioning
|
||||
|
||||
`format_version` is the scene schema version, not the Bevy Forge application version. It is currently
|
||||
`1` (`FORMAT_VERSION`).
|
||||
|
||||
- The editor writes `1`.
|
||||
- On load, version `0` is normalized to `1` in memory and the data is kept (a permissive path for
|
||||
early files).
|
||||
- There is no migration framework yet: the loader otherwise parses the document as the current
|
||||
schema. Treat version `1` as the only supported schema.
|
||||
- Additive fields should be serde-defaultable so older files still load; breaking changes bump
|
||||
`format_version`.
|
||||
- Generated code is not stored in the file and is not versioned by this field. Export behaviour is
|
||||
governed by the codegen contract and the target Bevy version (0.20).
|
||||
|
||||
## Asset paths
|
||||
|
||||
Sprite images and asset meshes use paths **relative to the project's Bevy assets directory**, with
|
||||
`/` separators:
|
||||
|
||||
```ron
|
||||
image: Some("textures/player.png")
|
||||
```
|
||||
|
||||
```ron
|
||||
value: "models/robot.gltf#Mesh0/Primitive0"
|
||||
```
|
||||
|
||||
Rules (violations are validation errors):
|
||||
|
||||
- No absolute paths.
|
||||
- No `..` path components.
|
||||
- No backslashes.
|
||||
- With an asset index, the file must exist and its kind must match the use (for example, an `.ogg`
|
||||
cannot be used as a sprite image; `.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tga`, `.ktx2` and
|
||||
`.dds` are images, while `.gltf`, `.glb`, `.obj` and `.fbx` are meshes).
|
||||
|
||||
Projects with an `assets/` directory use it directly. Projects without one treat the project root as
|
||||
the assets directory. The editor resolves asset paths through the project, never the process working
|
||||
directory.
|
||||
|
||||
## Saving and autosave
|
||||
|
||||
- `Project::save_scene` serializes with the pretty RON printer and writes **atomically**: it writes
|
||||
a temporary `.<filename>.tmp-<pid>` file in the destination directory and renames it over the
|
||||
target. A crash during save leaves the previous file intact.
|
||||
- The editor refuses non-`.forge.ron` filenames and `Project::save_scene` refuses paths outside the
|
||||
project root. Parent directories are created for save-as paths.
|
||||
- Saving marks the session clean; the title dot and quit guard use that flag.
|
||||
- When autosave is enabled (`autosave_secs > 0`, default 60 seconds), a dirty document is written to
|
||||
`<project>/.forge/autosave.forge.ron` at most once per interval. Autosave is best-effort: it does
|
||||
not mark the session saved and is not a substitute for saving. Set `autosave_secs` to 0 to disable
|
||||
it.
|
||||
- Editor preferences are stored separately in `bevy-forge/config.ron` under the platform config
|
||||
directory; that file is not a scene.
|
||||
|
||||
## Validation
|
||||
|
||||
`validate_scene(doc, assets: Option<&AssetIndex>)` returns a `ValidationReport` with `Error`,
|
||||
`Warning` and `Info` issues. `forge validate` prints them and uses the exit code to distinguish
|
||||
"valid" from "has errors"; the editor shows per-entity issues and summary counts. Passing `None` for
|
||||
the asset index disables missing-asset checks.
|
||||
|
||||
**Errors**
|
||||
|
||||
- Structural invariant failures: missing parent/child/root, cycles, mismatched node ID, duplicate
|
||||
references.
|
||||
- Non-finite transform, rotation or scale values.
|
||||
- Non-finite light or material values.
|
||||
- Custom component: invalid type path, invalid field name, empty field expression.
|
||||
- Asset reference containing an absolute path, a `..` component or a backslash.
|
||||
- With an asset index: referenced sprite image or mesh asset missing, or the wrong asset kind.
|
||||
- Sprite `custom_size` with a non-positive component.
|
||||
- Text `font_size <= 0`.
|
||||
- `PointLight.intensity < 0` or `range <= 0`.
|
||||
- `DirectionalLight.illuminance < 0`.
|
||||
- An entity is its own parent or introduces a cycle.
|
||||
|
||||
**Warnings**
|
||||
|
||||
- Duplicate entity names.
|
||||
- A sprite with no image and no custom size (it renders nothing).
|
||||
- A zero scale component.
|
||||
- Material `metallic` or `perceptual_roughness` outside `0..=1`.
|
||||
- Empty `Text2d` string.
|
||||
- Both 2D and 3D cameras in one scene.
|
||||
- An empty or whitespace-only entity name.
|
||||
|
||||
**Info**
|
||||
|
||||
- No camera in the scene.
|
||||
- No light in a scene that contains meshes.
|
||||
- An entity with only a `Transform`.
|
||||
- More than one camera.
|
||||
|
||||
Validation is deterministic (severity descending, then entity ID, then message) and must not panic
|
||||
on malformed forests.
|
||||
|
||||
## Relationship to BSN
|
||||
|
||||
The `.forge.ron` file is the source of truth in the editor. Exporting walks the forest and emits
|
||||
`Name`, `Transform` and the component list for every entity, converting Euler degrees to a quaternion
|
||||
and choosing asset parameters for mesh scenes. The generated file is portable Rust, not a scene asset
|
||||
that Bevy loads at runtime. See the [BSN export guide](bsn-export.md).
|
||||
Reference in New Issue
Block a user