Bevy Forge 0.1.0: visual scene editor for Bevy 0.20 BSN

This commit is contained in:
Emil Shanaty
2026-10-27 18:52:10 +03:00
commit 13bf16d510
79 changed files with 44353 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "forge-demo"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
bevy = "0.20"
# Standalone project on purpose: Bevy Forge opens this directory as a Bevy project,
# and the repository's editor workspace must not build Bevy.
[workspace]
+57
View File
@@ -0,0 +1,57 @@
# Forge Demo
A tiny Bevy 0.20 project used as the demo project for **Bevy Forge**, the visual scene
editor for Bevy BSN scenes. It contains two editable scenes and a small set of generated
pixel-art textures.
## Open it in Bevy Forge
From the repository root:
```bash
cargo run -p bevy_forge_editor -- examples/demo-project
```
Bevy Forge discovers the Bevy project from `Cargo.toml`, scans `assets/` and opens the
first scene automatically. Open the other scene from the File menu, edit entities in the
hierarchy/inspector, and save with `Ctrl+S`. Use **Export** (or the `forge` CLI) to turn a
scene into paste-ready BSN Rust code or imperative `Commands::spawn` code.
## Scenes
| Scene | Contents |
|---|---|
| `scenes/garden.forge.ron` | 3D garden: camera, sun, point light, ground plane, shrub, flower, lantern post and an orbiting crystal with the custom `forge_demo::Orbit` component. |
| `scenes/platformer.forge.ron` | 2D platformer: camera, dusk background, ground tiles, floating ledge, player with a hat child, coins, crates and two Text2d labels. |
Both scenes are plain RON and human-readable. Every referenced texture exists in
`assets/textures/`.
## Textures
All textures are procedurally generated (no external art, no network):
| File | Size |
|---|---|
| `assets/textures/background.png` | 960x540 |
| `assets/textures/player.png` | 64x96 |
| `assets/textures/hat.png` | 64x32 |
| `assets/textures/tile.png` | 96x96 |
| `assets/textures/coin.png` | 64x64 |
| `assets/textures/crate.png` | 64x64 |
Regenerate them with Pillow:
```bash
python3 examples/demo-project/tools/generate_textures.py
```
## Running it as a Bevy app
`src/main.rs` is a minimal Bevy app. The `.forge.ron` scenes are editor documents, not
Bevy runtime assets yet, so paste an export into `src/main.rs` to run it. The custom
`Orbit` component used by the garden scene is defined there as
`forge_demo::Orbit { radius, speed }`, matching the exported BSN path.
This demo is deliberately **not** a member of the editor workspace: only the excluded
`compat/bsn-smoke` crate may depend on Bevy.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

@@ -0,0 +1,437 @@
(
format_version: 1,
name: "Garden Demo",
entities: {
1: (
id: 1,
name: "Camera",
parent: None,
children: [],
transform: (
translation: (
x: 7.5,
y: 5.0,
z: 12.0,
),
rotation: (
x: -16.0,
y: 30.0,
z: 0.0,
),
scale: (
x: 1.0,
y: 1.0,
z: 1.0,
),
),
components: [
(
type: camera3d,
),
],
locked: false,
),
2: (
id: 2,
name: "Sun",
parent: None,
children: [],
transform: (
translation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
rotation: (
x: -55.0,
y: -25.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: 11000.0,
shadows: true,
),
),
],
locked: false,
),
3: (
id: 3,
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: plane(
size: (
x: 26.0,
y: 26.0,
),
),
),
material: (
base_color: (
r: 0.31,
g: 0.48,
b: 0.23,
a: 1.0,
),
metallic: 0.0,
perceptual_roughness: 0.92,
unlit: false,
),
),
),
],
locked: false,
),
4: (
id: 4,
name: "Shrub",
parent: None,
children: [],
transform: (
translation: (
x: -3.4,
y: 1.0,
z: 1.8,
),
rotation: (
x: 0.0,
y: 0.0,
z: 0.0,
),
scale: (
x: 1.25,
y: 0.9,
z: 1.25,
),
),
components: [
(
type: mesh3d,
data: (
mesh: (
source: primitive,
value: sphere(
radius: 1.0,
),
),
material: (
base_color: (
r: 0.24,
g: 0.45,
b: 0.22,
a: 1.0,
),
metallic: 0.0,
perceptual_roughness: 0.85,
unlit: false,
),
),
),
],
locked: false,
),
5: (
id: 5,
name: "Flower Stem",
parent: None,
children: [
6,
],
transform: (
translation: (
x: 2.6,
y: 0.75,
z: 1.4,
),
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: cylinder(
radius: 0.09,
height: 1.5,
),
),
material: (
base_color: (
r: 0.35,
g: 0.55,
b: 0.27,
a: 1.0,
),
metallic: 0.0,
perceptual_roughness: 0.8,
unlit: false,
),
),
),
],
locked: false,
),
6: (
id: 6,
name: "Flower Bloom",
parent: Some(5),
children: [],
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: 0.75,
z: 1.0,
),
),
components: [
(
type: mesh3d,
data: (
mesh: (
source: primitive,
value: sphere(
radius: 0.42,
),
),
material: (
base_color: (
r: 0.91,
g: 0.42,
b: 0.65,
a: 1.0,
),
metallic: 0.0,
perceptual_roughness: 0.4,
unlit: false,
),
),
),
],
locked: false,
),
7: (
id: 7,
name: "Orbit Crystal",
parent: None,
children: [],
transform: (
translation: (
x: 0.0,
y: 2.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: sphere(
radius: 0.8,
),
),
material: (
base_color: (
r: 0.27,
g: 0.76,
b: 0.69,
a: 1.0,
),
metallic: 0.15,
perceptual_roughness: 0.2,
unlit: false,
),
),
),
(
type: custom,
data: (
type_path: "forge_demo::Orbit",
fields: [
(
name: "radius",
expr: "2.4",
),
(
name: "speed",
expr: "0.45",
),
],
),
),
],
locked: false,
),
8: (
id: 8,
name: "Lantern Post",
parent: None,
children: [
9,
],
transform: (
translation: (
x: 3.8,
y: 1.2,
z: -1.2,
),
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: cylinder(
radius: 0.12,
height: 2.4,
),
),
material: (
base_color: (
r: 0.18,
g: 0.2,
b: 0.25,
a: 1.0,
),
metallic: 0.6,
perceptual_roughness: 0.35,
unlit: false,
),
),
),
],
locked: false,
),
9: (
id: 9,
name: "Lantern Glow",
parent: Some(8),
children: [],
transform: (
translation: (
x: 0.0,
y: 1.25,
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: point_light,
data: (
color: (
r: 1.0,
g: 0.82,
b: 0.55,
a: 1.0,
),
intensity: 600.0,
range: 12.0,
shadows: true,
),
),
],
locked: false,
),
},
roots: [
1,
2,
3,
4,
5,
7,
8,
],
next_id: 10,
)
@@ -0,0 +1,975 @@
(
format_version: 1,
name: "Platformer Demo",
entities: {
1: (
id: 1,
name: "Backdrop",
parent: None,
children: [],
transform: (
translation: (
x: 0.0,
y: 0.0,
z: -30.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/background.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 960.0,
y: 540.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
2: (
id: 2,
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,
),
3: (
id: 3,
name: "Title",
parent: None,
children: [],
transform: (
translation: (
x: 0.0,
y: 214.0,
z: 5.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 Platformer",
font_size: 44.0,
color: (
r: 0.96,
g: 0.94,
b: 0.88,
a: 1.0,
),
),
),
],
locked: false,
),
4: (
id: 4,
name: "Hint",
parent: None,
children: [],
transform: (
translation: (
x: 0.0,
y: 176.0,
z: 5.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: "Collect the coins!",
font_size: 20.0,
color: (
r: 0.95,
g: 0.67,
b: 0.25,
a: 1.0,
),
),
),
],
locked: false,
),
5: (
id: 5,
name: "Ground 1",
parent: None,
children: [],
transform: (
translation: (
x: -336.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
6: (
id: 6,
name: "Ground 2",
parent: None,
children: [],
transform: (
translation: (
x: -240.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
7: (
id: 7,
name: "Ground 3",
parent: None,
children: [],
transform: (
translation: (
x: -144.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
8: (
id: 8,
name: "Ground 4",
parent: None,
children: [],
transform: (
translation: (
x: -48.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
9: (
id: 9,
name: "Ground 5",
parent: None,
children: [],
transform: (
translation: (
x: 48.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
10: (
id: 10,
name: "Ground 6",
parent: None,
children: [],
transform: (
translation: (
x: 144.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
11: (
id: 11,
name: "Ground 7",
parent: None,
children: [],
transform: (
translation: (
x: 240.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
12: (
id: 12,
name: "Ground 8",
parent: None,
children: [],
transform: (
translation: (
x: 336.0,
y: -224.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
13: (
id: 13,
name: "Ledge 1",
parent: None,
children: [],
transform: (
translation: (
x: -48.0,
y: -64.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
14: (
id: 14,
name: "Ledge 2",
parent: None,
children: [],
transform: (
translation: (
x: 48.0,
y: -64.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
15: (
id: 15,
name: "Ledge 3",
parent: None,
children: [],
transform: (
translation: (
x: 144.0,
y: -64.0,
z: -2.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/tile.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 96.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
16: (
id: 16,
name: "Player",
parent: None,
children: [
17,
],
transform: (
translation: (
x: -192.0,
y: -128.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: 64.0,
y: 96.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
17: (
id: 17,
name: "Hat",
parent: Some(16),
children: [],
transform: (
translation: (
x: 0.0,
y: 42.0,
z: 1.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/hat.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 56.0,
y: 28.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
18: (
id: 18,
name: "Coin 1",
parent: None,
children: [],
transform: (
translation: (
x: -288.0,
y: -104.0,
z: 1.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/coin.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 48.0,
y: 48.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
19: (
id: 19,
name: "Coin 2",
parent: None,
children: [],
transform: (
translation: (
x: 48.0,
y: 28.0,
z: 1.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/coin.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 48.0,
y: 48.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
20: (
id: 20,
name: "Coin 3",
parent: None,
children: [],
transform: (
translation: (
x: 240.0,
y: -96.0,
z: 1.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/coin.png"),
color: (
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
),
custom_size: Some((
x: 48.0,
y: 48.0,
)),
flip_x: false,
flip_y: false,
),
),
],
locked: false,
),
21: (
id: 21,
name: "Crate",
parent: None,
children: [],
transform: (
translation: (
x: 352.0,
y: -144.0,
z: -1.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/crate.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,
),
),
],
locked: false,
),
22: (
id: 22,
name: "Crate 2",
parent: None,
children: [],
transform: (
translation: (
x: 352.0,
y: -80.0,
z: -1.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/crate.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,
),
),
],
locked: false,
),
},
roots: [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
18,
19,
20,
21,
22,
],
next_id: 23,
)
+57
View File
@@ -0,0 +1,57 @@
//! Minimal Bevy 0.20 app for the Bevy Forge demo project.
//!
//! Bevy Forge opens this directory as a Bevy project:
//!
//! ```text
//! cargo run -p bevy_forge_editor -- examples/demo-project
//! ```
//!
//! The files in `scenes/` are editor documents (`.forge.ron`), not runtime Bevy
//! assets. To run a scene, export it from Bevy Forge (or the `forge` CLI) and paste
//! the generated function into this file, then call it from a startup system:
//!
//! - a single-root scene uses `bsn!`, returns `impl Scene` and is spawned with
//! `commands.spawn_scene(scene(..))`;
//! - a multi-root scene uses `bsn_list!`, returns `impl SceneList` and is spawned
//! with `commands.spawn_scene_list(scene(..))`;
//! - generated mesh scenes take `&mut Assets<Mesh>` and
//! `&mut Assets<StandardMaterial>` parameters; the imperative Rust export also
//! takes `Commands` and `AssetServer`.
//!
//! The exported garden scene refers to the custom `forge_demo::Orbit` component, so
//! that type lives in this crate (see the module below). Keep it, or replace the
//! path in the generated code, when pasting an export here.
use bevy::prelude::*;
fn main() {
App::new().add_plugins(DefaultPlugins).run();
}
/// Project-specific components used by the demo scenes.
///
/// `scenes/garden.forge.ron` attaches `forge_demo::Orbit` to the Orbit Crystal, and
/// the BSN export emits `forge_demo::Orbit { radius: 2.4, speed: 0.45 }`. Deriving
/// `Component` plus `Default`/`Clone` is enough for Bevy 0.20's `FromTemplate`
/// blanket implementation, so the pasted scene compiles in this crate.
pub mod forge_demo {
use bevy::prelude::*;
/// Animates an entity in a circle around its spawn point.
#[derive(Component, Debug, Clone)]
pub struct Orbit {
/// Orbit radius in world units.
pub radius: f32,
/// Angular speed in radians per second.
pub speed: f32,
}
impl Default for Orbit {
fn default() -> Self {
Self {
radius: 2.4,
speed: 0.45,
}
}
}
}
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
"""Generate the pixel-art textures used by the Bevy Forge demo project.
All textures are drawn procedurally from a small palette so the demo stays
license-clean and can be regenerated on any machine with Pillow installed:
python3 examples/demo-project/tools/generate_textures.py
The output is written to ``examples/demo-project/assets/textures``.
"""
from __future__ import annotations
import random
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "assets" / "textures"
OUT.mkdir(parents=True, exist_ok=True)
# Shared palette (deliberately small, dusk / forest leaning).
OUTLINE = (43, 32, 51, 255)
SKIN = (244, 201, 158, 255)
SKIN_SHADOW = (217, 164, 122, 255)
HAIR = (122, 78, 45, 255)
TUNIC = (54, 126, 143, 255)
TUNIC_DARK = (38, 95, 110, 255)
BELT = (91, 62, 38, 255)
BUCKLE = (242, 179, 61, 255)
PANTS = (64, 79, 105, 255)
BOOT = (74, 56, 43, 255)
CREAM = (255, 244, 214, 255)
def logical(width: int, height: int) -> tuple[Image.Image, ImageDraw.ImageDraw]:
image = Image.new("RGBA", (width, height), (0, 0, 0, 0))
return image, ImageDraw.Draw(image)
def upscale(image: Image.Image, factor: int) -> Image.Image:
return image.resize(
(image.width * factor, image.height * factor),
Image.Resampling.NEAREST,
)
def save(image: Image.Image, name: str) -> None:
image.save(OUT / name, optimize=True)
print(f"wrote {name}: {image.width}x{image.height}")
def draw_player() -> None:
image, draw = logical(16, 24)
# Boots.
draw.rectangle((3, 20, 7, 23), fill=OUTLINE)
draw.rectangle((8, 20, 12, 23), fill=OUTLINE)
draw.rectangle((4, 21, 7, 22), fill=BOOT)
draw.rectangle((8, 21, 11, 22), fill=BOOT)
# Legs.
draw.rectangle((4, 16, 11, 20), fill=OUTLINE)
draw.rectangle((5, 17, 10, 20), fill=PANTS)
draw.rectangle((7, 17, 7, 19), fill=TUNIC_DARK)
# Torso and arms.
draw.rectangle((3, 10, 12, 16), fill=OUTLINE)
draw.rectangle((4, 11, 11, 15), fill=TUNIC)
draw.rectangle((2, 11, 3, 15), fill=OUTLINE)
draw.rectangle((12, 11, 13, 15), fill=OUTLINE)
draw.rectangle((3, 12, 3, 14), fill=TUNIC)
draw.rectangle((12, 12, 12, 14), fill=TUNIC)
draw.rectangle((3, 16, 4, 16), fill=SKIN)
draw.rectangle((11, 16, 12, 16), fill=SKIN)
draw.rectangle((3, 14, 12, 15), fill=BELT)
draw.rectangle((7, 14, 8, 15), fill=BUCKLE)
draw.rectangle((4, 11, 11, 11), fill=TUNIC_DARK)
# Head with a friendly face.
draw.rectangle((4, 2, 11, 10), fill=OUTLINE)
draw.rectangle((5, 3, 10, 9), fill=SKIN)
draw.rectangle((5, 2, 10, 4), fill=HAIR)
draw.rectangle((4, 4, 4, 6), fill=HAIR)
draw.rectangle((11, 4, 11, 6), fill=HAIR)
draw.rectangle((6, 6, 6, 6), fill=OUTLINE)
draw.rectangle((9, 6, 9, 6), fill=OUTLINE)
draw.rectangle((7, 8, 8, 8), fill=SKIN_SHADOW)
draw.rectangle((5, 8, 5, 8), fill=SKIN_SHADOW)
draw.rectangle((10, 8, 10, 8), fill=SKIN_SHADOW)
save(upscale(image, 4), "player.png")
def draw_hat() -> None:
image, draw = logical(16, 8)
hat = (228, 87, 46, 255)
hat_shine = (244, 138, 54, 255)
brim = (188, 62, 34, 255)
hat_outline = (63, 35, 28, 255)
draw.rectangle((3, 1, 12, 5), fill=hat_outline)
draw.rectangle((4, 2, 11, 4), fill=hat)
draw.rectangle((5, 2, 8, 3), fill=hat_shine)
draw.rectangle((7, 0, 8, 0), fill=hat_outline)
draw.rectangle((1, 5, 14, 7), fill=hat_outline)
draw.rectangle((2, 5, 13, 6), fill=brim)
draw.rectangle((4, 4, 11, 4), fill=brim)
save(upscale(image, 4), "hat.png")
def draw_tile() -> None:
image, draw = logical(24, 24)
dirt = (122, 78, 45, 255)
dirt_dark = (92, 58, 34, 255)
dirt_light = (155, 118, 75, 255)
grass = (78, 159, 61, 255)
grass_light = (111, 207, 95, 255)
grass_dark = (46, 91, 38, 255)
draw.rectangle((0, 0, 23, 23), fill=dirt)
draw.rectangle((0, 0, 23, 1), fill=grass_light)
draw.rectangle((0, 2, 23, 5), fill=grass)
draw.rectangle((0, 6, 23, 6), fill=grass_dark)
draw.rectangle((0, 7, 23, 7), fill=dirt_dark)
# Grass dribbling over the soil edge.
for x in (2, 3, 8, 9, 15, 20, 21):
draw.rectangle((x, 7, x, 8), fill=grass)
draw.rectangle((3, 7, 6, 8), fill=grass_dark)
draw.rectangle((16, 7, 18, 9), fill=grass_dark)
# Soil speckles and stones.
rng = random.Random(11)
for _ in range(16):
x = rng.randint(1, 21)
y = rng.randint(10, 22)
draw.rectangle((x, y, x + 1, y), fill=dirt_dark)
for _ in range(8):
x = rng.randint(2, 20)
y = rng.randint(12, 21)
draw.rectangle((x, y, x, y), fill=dirt_light)
draw.rectangle((5, 14, 7, 14), fill=(155, 122, 83, 255))
draw.rectangle((17, 18, 19, 18), fill=(155, 122, 83, 255))
draw.rectangle((0, 22, 23, 23), fill=dirt_dark)
# Tiny flowers so a long ground row reads as a garden.
for x in (6, 19):
draw.rectangle((x, 1, x, 1), fill=CREAM)
draw.rectangle((x - 1, 1, x + 1, 1), fill=CREAM)
draw.rectangle((x, 0, x, 0), fill=CREAM)
draw.rectangle((x, 3, x, 3), fill=(78, 159, 61, 255))
draw.rectangle((x, 2, x, 2), fill=(242, 179, 61, 255))
save(upscale(image, 4), "tile.png")
def draw_coin() -> None:
image, draw = logical(16, 16)
draw.ellipse((1, 1, 14, 14), fill=(140, 90, 22, 255))
draw.ellipse((2, 2, 13, 13), fill=(242, 179, 61, 255))
draw.ellipse((4, 4, 11, 11), fill=(255, 217, 102, 255))
draw.rectangle((7, 5, 8, 10), fill=CREAM)
draw.rectangle((5, 7, 10, 8), fill=CREAM)
draw.rectangle((5, 4, 6, 4), fill=(255, 255, 255, 255))
draw.rectangle((4, 5, 4, 5), fill=(255, 255, 255, 255))
save(upscale(image, 4), "coin.png")
def draw_crate() -> None:
image, draw = logical(16, 16)
outline = (63, 43, 28, 255)
wood = (169, 116, 63, 255)
wood_light = (201, 143, 82, 255)
wood_dark = (122, 78, 45, 255)
draw.rectangle((0, 0, 15, 15), fill=outline)
draw.rectangle((1, 1, 14, 14), fill=wood)
draw.rectangle((1, 1, 14, 1), fill=wood_light)
draw.rectangle((1, 14, 14, 14), fill=wood_dark)
draw.rectangle((1, 1, 3, 14), fill=wood_light)
draw.rectangle((12, 1, 14, 14), fill=wood_dark)
draw.line((4, 3, 11, 12), fill=wood_dark)
draw.line((11, 3, 4, 12), fill=wood_dark)
for x, y in ((2, 2), (13, 2), (2, 13), (13, 13)):
draw.rectangle((x, y, x, y), fill=(217, 187, 130, 255))
save(upscale(image, 4), "crate.png")
def dusk_gradient(t: float) -> tuple[int, int, int]:
stops = [
(0.00, (15, 14, 34)),
(0.42, (45, 34, 70)),
(0.62, (96, 58, 78)),
(0.74, (176, 92, 70)),
(0.82, (232, 146, 84)),
(0.90, (244, 180, 110)),
(1.00, (28, 26, 48)),
]
for index in range(len(stops) - 1):
start_t, start = stops[index]
end_t, end = stops[index + 1]
if start_t <= t <= end_t:
span = max(end_t - start_t, 1e-6)
blend = (t - start_t) / span
return tuple(
round(start[channel] + (end[channel] - start[channel]) * blend)
for channel in range(3)
)
return stops[-1][1]
def draw_background() -> None:
width, height = 240, 135
image = Image.new("RGB", (width, height))
draw = ImageDraw.Draw(image)
for y in range(height):
draw.line(((0, y), (width, y)), fill=dusk_gradient(y / (height - 1)))
# Stars.
rng = random.Random(7)
for _ in range(46):
x = rng.randint(2, width - 3)
y = rng.randint(3, 62)
brightness = rng.choice([(255, 255, 255), (255, 244, 214), (210, 205, 235)])
draw.point((x, y), fill=brightness)
if rng.random() < 0.18:
draw.point((x + 1, y), fill=brightness)
# Soft evening clouds.
draw.rectangle((0, 26, width, 28), fill=(66, 52, 92))
draw.rectangle((20, 38, 190, 39), fill=(78, 60, 104))
draw.rectangle((60, 47, 230, 48), fill=(90, 66, 105))
# Sun low over the horizon.
draw.ellipse((150, 64, 191, 105), fill=(247, 190, 120))
draw.ellipse((157, 71, 184, 98), fill=(255, 226, 164))
draw.ellipse((162, 76, 179, 93), fill=(255, 244, 214))
# Far mountain range.
draw.polygon(
[
(0, 101),
(25, 82),
(48, 94),
(72, 76),
(95, 90),
(120, 80),
(150, 97),
(175, 84),
(205, 96),
(230, 88),
(240, 96),
(240, 135),
(0, 135),
],
fill=(66, 52, 96),
)
# Mid range.
draw.polygon(
[
(0, 112),
(30, 100),
(55, 108),
(80, 94),
(110, 106),
(140, 98),
(168, 110),
(196, 100),
(220, 108),
(240, 102),
(240, 135),
(0, 135),
],
fill=(47, 38, 72),
)
# Rolling hills.
draw.polygon(
[
(0, 123),
(35, 114),
(70, 121),
(110, 112),
(145, 120),
(180, 113),
(215, 121),
(240, 115),
(240, 135),
(0, 135),
],
fill=(31, 29, 52),
)
# Foreground silhouette with a few conifers.
draw.polygon(
[
(0, 129),
(30, 126),
(60, 129),
(90, 125),
(120, 129),
(150, 126),
(180, 130),
(210, 126),
(240, 129),
(240, 135),
(0, 135),
],
fill=(20, 19, 34),
)
for x, base in ((28, 129), (66, 130), (148, 127), (196, 131)):
draw.polygon([(x, base - 12), (x - 4, base), (x + 4, base)], fill=(17, 24, 30))
draw.rectangle((x - 1, base - 13, x + 1, base - 10), fill=(17, 24, 30))
draw.rectangle((x, base - 14, x, base - 13), fill=(17, 24, 30))
for x in range(4, width, 17):
draw.point((x, 132), fill=(36, 55, 42))
image = upscale(image, 4)
image = image.quantize(colors=64, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
save(image.convert("RGB"), "background.png")
def main() -> None:
draw_background()
draw_player()
draw_hat()
draw_tile()
draw_coin()
draw_crate()
total = sum(path.stat().st_size for path in OUT.glob("*.png"))
print(f"total texture size: {total} bytes")
if __name__ == "__main__":
main()