eulumdat_bevy/
scene.rs

1//! Scene geometry generation
2
3use crate::SceneSettings;
4use bevy::pbr::NotShadowCaster;
5use bevy::prelude::*;
6
7pub struct ScenePlugin;
8
9impl Plugin for ScenePlugin {
10    fn build(&self, app: &mut App) {
11        // Initialize default SceneSettings if not already present
12        app.init_resource::<SceneSettings>();
13        app.add_systems(
14            Startup,
15            setup_scene.run_if(resource_exists::<SceneSettings>),
16        )
17        .add_systems(
18            Update,
19            rebuild_scene_on_change.run_if(resource_exists::<SceneSettings>),
20        );
21    }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum SceneType {
26    #[default]
27    Room,
28    Road,
29    Parking,
30    Outdoor,
31}
32
33impl SceneType {
34    pub fn default_dimensions(&self) -> (f32, f32, f32, f32) {
35        // (width, length, height, mount_height)
36        match self {
37            SceneType::Room => (4.0, 5.0, 2.8, 2.5),
38            SceneType::Road => (10.0, 30.0, 0.0, 8.0),
39            SceneType::Parking => (20.0, 30.0, 0.0, 6.0),
40            SceneType::Outdoor => (10.0, 15.0, 0.0, 3.0),
41        }
42    }
43}
44
45#[derive(Component)]
46pub struct SceneGeometry;
47
48fn setup_scene(
49    mut commands: Commands,
50    mut meshes: ResMut<Assets<Mesh>>,
51    mut materials: ResMut<Assets<StandardMaterial>>,
52    settings: Res<SceneSettings>,
53) {
54    build_scene(&mut commands, &mut meshes, &mut materials, &settings);
55}
56
57fn rebuild_scene_on_change(
58    mut commands: Commands,
59    mut meshes: ResMut<Assets<Mesh>>,
60    mut materials: ResMut<Assets<StandardMaterial>>,
61    settings: Res<SceneSettings>,
62    query: Query<Entity, With<SceneGeometry>>,
63) {
64    if !settings.is_changed() {
65        return;
66    }
67
68    // Remove old scene geometry
69    for entity in query.iter() {
70        commands.entity(entity).despawn_recursive();
71    }
72
73    // Build new scene
74    build_scene(&mut commands, &mut meshes, &mut materials, &settings);
75}
76
77fn build_scene(
78    commands: &mut Commands,
79    meshes: &mut ResMut<Assets<Mesh>>,
80    materials: &mut ResMut<Assets<StandardMaterial>>,
81    settings: &SceneSettings,
82) {
83    match settings.scene_type {
84        SceneType::Room => build_room(commands, meshes, materials, settings),
85        SceneType::Road => build_road(commands, meshes, materials, settings),
86        SceneType::Parking => build_parking(commands, meshes, materials, settings),
87        SceneType::Outdoor => build_outdoor(commands, meshes, materials, settings),
88    }
89
90    // Add ambient light - keep low so luminaire effect is visible
91    commands.insert_resource(AmbientLight {
92        color: Color::srgb(0.9, 0.9, 1.0),
93        brightness: 50.0, // Low ambient to see lighting differences
94    });
95}
96
97fn build_room(
98    commands: &mut Commands,
99    meshes: &mut ResMut<Assets<Mesh>>,
100    materials: &mut ResMut<Assets<StandardMaterial>>,
101    settings: &SceneSettings,
102) {
103    let w = settings.room_width;
104    let l = settings.room_length;
105    let h = settings.room_height;
106
107    // Floor
108    let floor_material = materials.add(StandardMaterial {
109        base_color: Color::srgb(0.85, 0.85, 0.85),
110        perceptual_roughness: 0.8,
111        ..default()
112    });
113
114    commands.spawn((
115        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
116        MeshMaterial3d(floor_material.clone()),
117        Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
118        SceneGeometry,
119    ));
120
121    // Ceiling
122    let ceiling_material = materials.add(StandardMaterial {
123        base_color: Color::srgb(0.95, 0.95, 0.95),
124        perceptual_roughness: 0.9,
125        ..default()
126    });
127
128    commands.spawn((
129        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
130        MeshMaterial3d(ceiling_material),
131        Transform::from_xyz(w / 2.0, h, l / 2.0)
132            .with_rotation(Quat::from_rotation_x(std::f32::consts::PI)),
133        SceneGeometry,
134    ));
135
136    // Walls
137    let wall_material = materials.add(StandardMaterial {
138        base_color: Color::srgb(0.95, 0.95, 0.95),
139        perceptual_roughness: 0.9,
140        ..default()
141    });
142
143    // Back wall (z=0)
144    commands.spawn((
145        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
146        MeshMaterial3d(wall_material.clone()),
147        Transform::from_xyz(w / 2.0, h / 2.0, 0.0)
148            .with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
149        SceneGeometry,
150    ));
151
152    // Front wall (z=l)
153    commands.spawn((
154        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
155        MeshMaterial3d(wall_material.clone()),
156        Transform::from_xyz(w / 2.0, h / 2.0, l)
157            .with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)),
158        SceneGeometry,
159    ));
160
161    // Left wall (x=0)
162    commands.spawn((
163        Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
164        MeshMaterial3d(wall_material.clone()),
165        Transform::from_xyz(0.0, h / 2.0, l / 2.0)
166            .with_rotation(Quat::from_rotation_z(-std::f32::consts::FRAC_PI_2)),
167        SceneGeometry,
168    ));
169
170    // Right wall (x=w)
171    commands.spawn((
172        Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
173        MeshMaterial3d(wall_material),
174        Transform::from_xyz(w, h / 2.0, l / 2.0)
175            .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
176        SceneGeometry,
177    ));
178}
179
180fn build_road(
181    commands: &mut Commands,
182    meshes: &mut ResMut<Assets<Mesh>>,
183    materials: &mut ResMut<Assets<StandardMaterial>>,
184    settings: &SceneSettings,
185) {
186    let w = settings.room_width;
187    let l = settings.room_length;
188
189    // Asphalt road
190    let road_material = materials.add(StandardMaterial {
191        base_color: Color::srgb(0.15, 0.15, 0.15),
192        perceptual_roughness: 0.9,
193        ..default()
194    });
195
196    commands.spawn((
197        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
198        MeshMaterial3d(road_material),
199        Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
200        SceneGeometry,
201    ));
202
203    // Road markings (center line)
204    let marking_material = materials.add(StandardMaterial {
205        base_color: Color::WHITE,
206        emissive: LinearRgba::new(0.3, 0.3, 0.3, 1.0),
207        ..default()
208    });
209
210    let mut z = 2.0;
211    while z < l - 2.0 {
212        commands.spawn((
213            Mesh3d(meshes.add(Cuboid::new(0.15, 0.02, 2.0))),
214            MeshMaterial3d(marking_material.clone()),
215            Transform::from_xyz(w / 2.0, 0.01, z),
216            SceneGeometry,
217        ));
218        z += 4.0;
219    }
220
221    // Sidewalks
222    let sidewalk_material = materials.add(StandardMaterial {
223        base_color: Color::srgb(0.6, 0.6, 0.6),
224        perceptual_roughness: 0.8,
225        ..default()
226    });
227
228    commands.spawn((
229        Mesh3d(meshes.add(Cuboid::new(1.0, 0.1, l))),
230        MeshMaterial3d(sidewalk_material.clone()),
231        Transform::from_xyz(0.5, 0.05, l / 2.0),
232        SceneGeometry,
233    ));
234
235    commands.spawn((
236        Mesh3d(meshes.add(Cuboid::new(1.0, 0.1, l))),
237        MeshMaterial3d(sidewalk_material),
238        Transform::from_xyz(w - 0.5, 0.05, l / 2.0),
239        SceneGeometry,
240    ));
241
242    // Light pole - on the right sidewalk, not on the road!
243    spawn_pole(
244        commands,
245        meshes,
246        materials,
247        Vec3::new(w - 0.7, 0.0, l / 2.0),
248        settings.mounting_height,
249    );
250}
251
252fn build_parking(
253    commands: &mut Commands,
254    meshes: &mut ResMut<Assets<Mesh>>,
255    materials: &mut ResMut<Assets<StandardMaterial>>,
256    settings: &SceneSettings,
257) {
258    let w = settings.room_width;
259    let l = settings.room_length;
260
261    // Parking lot surface
262    let lot_material = materials.add(StandardMaterial {
263        base_color: Color::srgb(0.2, 0.2, 0.2),
264        perceptual_roughness: 0.85,
265        ..default()
266    });
267
268    commands.spawn((
269        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
270        MeshMaterial3d(lot_material),
271        Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
272        SceneGeometry,
273    ));
274
275    // Parking lines
276    let line_material = materials.add(StandardMaterial {
277        base_color: Color::WHITE,
278        emissive: LinearRgba::new(0.2, 0.2, 0.2, 1.0),
279        ..default()
280    });
281
282    let space_width = 2.5;
283    let space_length = 5.0;
284
285    let mut row = 3.0;
286    while row < l - 3.0 {
287        let mut col = space_width;
288        while col < w - 1.0 {
289            commands.spawn((
290                Mesh3d(meshes.add(Cuboid::new(0.1, 0.02, space_length))),
291                MeshMaterial3d(line_material.clone()),
292                Transform::from_xyz(col, 0.01, row),
293                SceneGeometry,
294            ));
295            col += space_width;
296        }
297        row += space_length + 1.0;
298    }
299
300    // Light pole
301    spawn_pole(
302        commands,
303        meshes,
304        materials,
305        Vec3::new(w / 2.0, 0.0, l / 2.0),
306        settings.mounting_height,
307    );
308}
309
310fn build_outdoor(
311    commands: &mut Commands,
312    meshes: &mut ResMut<Assets<Mesh>>,
313    materials: &mut ResMut<Assets<StandardMaterial>>,
314    settings: &SceneSettings,
315) {
316    let w = settings.room_width;
317    let l = settings.room_length;
318
319    // Grass
320    let grass_material = materials.add(StandardMaterial {
321        base_color: Color::srgb(0.15, 0.3, 0.1),
322        perceptual_roughness: 0.95,
323        ..default()
324    });
325
326    commands.spawn((
327        Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
328        MeshMaterial3d(grass_material),
329        Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
330        SceneGeometry,
331    ));
332
333    // Garden path
334    let path_material = materials.add(StandardMaterial {
335        base_color: Color::srgb(0.5, 0.5, 0.5),
336        perceptual_roughness: 0.8,
337        ..default()
338    });
339
340    commands.spawn((
341        Mesh3d(meshes.add(Cuboid::new(1.2, 0.02, l - 2.0))),
342        MeshMaterial3d(path_material),
343        Transform::from_xyz(w / 2.0, 0.01, l / 2.0),
344        SceneGeometry,
345    ));
346
347    // Bushes
348    let bush_material = materials.add(StandardMaterial {
349        base_color: Color::srgb(0.1, 0.25, 0.05),
350        perceptual_roughness: 0.95,
351        ..default()
352    });
353
354    for (x, y, z) in [
355        (2.0, 0.4, 3.0),
356        (w - 2.0, 0.3, l - 4.0),
357        (1.5, 0.35, l - 2.0),
358    ] {
359        commands.spawn((
360            Mesh3d(meshes.add(Sphere::new(y))),
361            MeshMaterial3d(bush_material.clone()),
362            Transform::from_xyz(x, y, z),
363            SceneGeometry,
364        ));
365    }
366
367    // Light pole
368    spawn_pole(
369        commands,
370        meshes,
371        materials,
372        Vec3::new(w / 2.0, 0.0, l / 2.0),
373        settings.mounting_height,
374    );
375}
376
377fn spawn_pole(
378    commands: &mut Commands,
379    meshes: &mut ResMut<Assets<Mesh>>,
380    materials: &mut ResMut<Assets<StandardMaterial>>,
381    position: Vec3,
382    height: f32,
383) {
384    let pole_material = materials.add(StandardMaterial {
385        base_color: Color::srgb(0.4, 0.4, 0.4),
386        metallic: 0.6,
387        perceptual_roughness: 0.4,
388        ..default()
389    });
390
391    // Pole - don't cast shadows to avoid blocking the lamp
392    commands.spawn((
393        Mesh3d(meshes.add(Cylinder::new(0.08, height - 0.3))),
394        MeshMaterial3d(pole_material.clone()),
395        Transform::from_xyz(position.x, height / 2.0, position.z),
396        SceneGeometry,
397        NotShadowCaster,
398    ));
399
400    // Arm - short stub, luminaire hangs separately below
401    commands.spawn((
402        Mesh3d(meshes.add(Cylinder::new(0.05, 0.3))),
403        MeshMaterial3d(pole_material),
404        Transform::from_xyz(position.x - 0.05, height - 0.2, position.z)
405            .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
406        SceneGeometry,
407        NotShadowCaster,
408    ));
409}