Skip to content

Commit 6dd623d

Browse files
authored
Merge pull request #50 from voshch/feature/ceilings
ceilings :DDDD
2 parents 44ebfe1 + d3fcb58 commit 6dd623d

14 files changed

Lines changed: 212 additions & 19 deletions

File tree

arena_bringup/configs/gazebo/empty.sdf

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ambient>0.6 0.6 0.6 1</ambient>
66
<background>0.7 0.7 0.8 1</background>
77
<sky></sky>
8-
<shadows>true</shadows>
8+
<shadows>false</shadows>
99
</scene>
1010

1111
<!-- GPS reference: Waterloo, ON (from former GAZEBO_WORLD_LAT/LON env defaults in hector plugin) -->
@@ -48,18 +48,31 @@
4848
<real_time_update_rate>0</real_time_update_rate>
4949
</physics>
5050

51+
<!-- Shadowless rig: four directional lights from each side, downward-biased, for even
52+
lighting with no cast shadows. ogre2 PBR is lit by these, not by scene ambient. -->
5153
<light type="directional" name="sun">
52-
<cast_shadows>true</cast_shadows>
53-
<pose>0 0 10 0 0 0</pose>
54-
<diffuse>0.8 0.8 0.8 1</diffuse>
55-
<specular>0.2 0.2 0.2 1</specular>
56-
<attenuation>
57-
<range>1000</range>
58-
<constant>0.9</constant>
59-
<linear>0.01</linear>
60-
<quadratic>0.001</quadratic>
61-
</attenuation>
62-
<direction>-0.5 0.1 -0.9</direction>
54+
<cast_shadows>false</cast_shadows>
55+
<diffuse>0.45 0.45 0.45 1</diffuse>
56+
<specular>0 0 0 1</specular>
57+
<direction>-1 0 -0.35</direction>
58+
</light>
59+
<light type="directional" name="fill_west">
60+
<cast_shadows>false</cast_shadows>
61+
<diffuse>0.45 0.45 0.45 1</diffuse>
62+
<specular>0 0 0 1</specular>
63+
<direction>1 0 -0.35</direction>
64+
</light>
65+
<light type="directional" name="fill_north">
66+
<cast_shadows>false</cast_shadows>
67+
<diffuse>0.45 0.45 0.45 1</diffuse>
68+
<specular>0 0 0 1</specular>
69+
<direction>0 -1 -0.35</direction>
70+
</light>
71+
<light type="directional" name="fill_south">
72+
<cast_shadows>false</cast_shadows>
73+
<diffuse>0.45 0.45 0.45 1</diffuse>
74+
<specular>0 0 0 1</specular>
75+
<direction>0 1 -0.35</direction>
6376
</light>
6477

6578
<model name="ground_plane">

arena_runtime/arena_runtime/arena_runtime/sim/_interface.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections.abc import Iterable, Mapping, Sequence
77

88
from arena_people_msgs.msg import Pedestrians
9+
from arena_simulation_setup.shared import Ceiling
910
from task_generator.shared import (
1011
Door,
1112
DynamicObstacle,
@@ -131,6 +132,12 @@ async def spawn_floors(self, floors: Sequence[Floor]) -> bool:
131132
"""
132133
raise NotImplementedError()
133134

135+
async def spawn_ceilings(self, ceilings: Sequence[Ceiling]) -> bool:
136+
"""
137+
Add a list of ceilings to the simulator. No-op by default.
138+
"""
139+
return True
140+
134141
async def remove_world(self) -> bool:
135142
"""
136143
Remove every spawned world element.

arena_runtime/arena_runtime/arena_runtime/sim/gazebo_simulator/gazebo_simulator.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from arena_rclpy_mixins import ArenaMixinNode
2323
from arena_rclpy_mixins.Async import ClientWrapper
2424
from arena_rclpy_mixins.shared import Namespace
25+
from arena_simulation_setup.shared import Ceiling
2526
from arena_simulation_setup.tree.Wall import WallSegment
2627
from arena_simulation_setup.utils.material import MdlUtil
2728
from geometry_msgs.msg import PoseWithCovarianceStamped
@@ -287,6 +288,16 @@ async def spawn_floors(self, floors: Sequence[Floor]) -> bool:
287288
self._walls_entities.append(name)
288289
return True
289290

291+
async def spawn_ceilings(self, ceilings: Sequence[Ceiling]) -> bool:
292+
for ceiling in ceilings:
293+
name = self._realizer.realize(f"ceiling_{next(self._wall_counter)}")
294+
textures = await self._resolve_wall_textures(ceiling.material)
295+
ceiling_sdf = _generate_ceiling_sdf(name, ceiling, textures)
296+
async with self._semaphore:
297+
await self._spawn_sdf(name, ceiling_sdf, Pose())
298+
self._walls_entities.append(name)
299+
return True
300+
290301
def robot_positions_xy(self) -> Iterable[tuple[str, tuple[float, float]]]:
291302
out: list[tuple[str, tuple[float, float]]] = []
292303
for sim_path, frame in list(self._agent_robots.items()):
@@ -877,6 +888,32 @@ def _generate_floor_sdf(name: str, floor: Floor, textures: dict[str, str]) -> st
877888
"""
878889

879890

891+
def _generate_ceiling_sdf(name: str, ceiling: Ceiling, textures: dict[str, str]) -> str:
892+
"""SDF model with a single downward-facing plane, visible only from below."""
893+
cast_shadows = 'true' if ceiling.cast_shadows else 'false'
894+
return f"""
895+
<sdf version="1.6">
896+
<model name="{name}">
897+
<pose>0 0 0 0 0 0</pose>
898+
<link name="ceiling_link">
899+
<visual name="visual">
900+
<cast_shadows>{cast_shadows}</cast_shadows>
901+
<geometry>
902+
<plane>
903+
<normal>0 0 -1</normal>
904+
<size>{ceiling.x_length} {ceiling.y_length}</size>
905+
</plane>
906+
</geometry>
907+
{_wall_material_sdf(textures)}
908+
</visual>
909+
<pose>{ceiling.pos.x} {ceiling.pos.y} {ceiling.z} 0 0 0</pose>
910+
</link>
911+
<static>true</static>
912+
</model>
913+
</sdf>
914+
"""
915+
916+
880917
_BOX_SDF_TEMPLATE = """
881918
<sdf version="1.6">
882919
<model name="{name}">

arena_runtime/arena_runtime/arena_runtime/sim/isaac_simulator.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from arena_simulation_setup.shared import Obstacle as ObstacleDefinition
3434
from arena_simulation_setup.tree.Wall import WallSegment
3535
from isaacsim_msgs.msg import (
36+
Ceiling,
3637
Floor,
3738
Material,
3839
Prim,
@@ -42,12 +43,14 @@
4243
DeletePrims,
4344
EditPrims,
4445
ResetWorld,
46+
SpawnCeilings,
4547
SpawnFloors,
4648
SpawnPrims,
4749
SpawnUrdf,
4850
SpawnUsd,
4951
SpawnWalls,
5052
)
53+
from task_generator.shared import Ceiling as CeilingDefinition
5154
from task_generator.shared import (
5255
DynamicObstacle,
5356
Model,
@@ -194,6 +197,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
194197
self._NS_ROBOT = Namespace(env_prefix)('Robots')
195198
self._NS_WALL = Namespace(env_prefix)('Walls')
196199
self._NS_FLOOR = Namespace(env_prefix)('Floors')
200+
self._NS_CEILING = Namespace(env_prefix)('Ceilings')
197201

198202
self.wall_counter = itertools.count()
199203
self.floor_counter = itertools.count()
@@ -204,6 +208,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
204208
MovePedestrians=self.node.create_client_wrapper(MovePedestrians, "/isaac/MovePedestrians"),
205209
ResetWorld=self.node.create_client_wrapper(ResetWorld, "/isaac/ResetWorld"),
206210
UpdatePedestrians=self.node.create_client_wrapper(UpdatePedestrians, "/isaac/UpdatePedestrians"),
211+
SpawnCeilings=self.node.create_client_wrapper(SpawnCeilings, "/isaac/SpawnCeilings"),
207212
SpawnFloors=self.node.create_client_wrapper(SpawnFloors, "/isaac/SpawnFloors"),
208213
SpawnPedestrians=self.node.create_client_wrapper(SpawnPedestrians, "/isaac/SpawnPedestrians"),
209214
SpawnPrims=self.node.create_client_wrapper(SpawnPrims, "/isaac/SpawnPrims"),
@@ -535,6 +540,34 @@ async def impl(floor: FloorDefinition) -> Floor | None:
535540
self._logger.debug("All floors spawned successfully.")
536541
return res
537542

543+
async def spawn_ceilings(self, ceilings: Sequence[CeilingDefinition]) -> bool:
544+
self._logger.debug("Attempting to spawn ceilings")
545+
546+
async def impl(ceiling: CeilingDefinition) -> Ceiling | None:
547+
try:
548+
pos = ceiling.pos.to_msg()
549+
pos.z = ceiling.z
550+
return Ceiling(
551+
name=self._NS_CEILING(ceiling.name),
552+
x_length=ceiling.x_length,
553+
y_length=ceiling.y_length,
554+
pos=pos,
555+
cast_shadows=ceiling.cast_shadows,
556+
material=material_to_msg(await ceiling.material.resolve()),
557+
)
558+
559+
except Exception:
560+
self._logger.error(f"Failed to spawn ceiling: {ceiling.name}\n{traceback.format_exc()}")
561+
return None
562+
563+
ceilings_req = SpawnCeilings.Request()
564+
ceilings_req.ceilings = list(filter(None, await asyncio.gather(*map(impl, ceilings))))
565+
ceilings_res = await self._clients.SpawnCeilings.call_timeout(ceilings_req)
566+
567+
res = bool(ceilings_res) and all(ceilings_res.ret)
568+
self._logger.debug("All ceilings spawned successfully.")
569+
return res
570+
538571
async def spawn_box(self, name: str, size: tuple[float, float, float], pose: Pose) -> bool:
539572
"""Spawn an axis-aligned box using the SpawnWalls service.
540573

arena_simulation_setup/AUTHORING.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,29 @@ Key points:
6969
- `material` on a zone accepts the same forms as `MaterialIdentifier`: a plain
7070
string `Concrete_Smooth`, or a two-element list `[name, modifiers_dict]`.
7171
- Multiple zones share a single flat `zones` list. Zone boundaries are defined
72-
only by their `corners` polygon and their `walls` list — there is no
73-
explicit parent–child relationship.
72+
only by their `corners` polygon and their `walls` list, with no explicit
73+
parent-child relationship.
74+
75+
### Ceilings
76+
77+
Add a ceiling to any zone with these optional keys:
78+
79+
```yaml
80+
ceiling: true # true by default, set false to leave the zone open
81+
ceiling_height: 3.0 # top height in metres, omit to derive from wall stack
82+
ceiling_cast_shadows: false # false by default, true to let the ceiling occlude light
83+
ceiling_material: # MaterialIdentifier, defaults to Concrete_Smooth
84+
- Concrete_Smooth
85+
- {}
86+
```
87+
88+
With `ceiling_height` absent, the height is the tallest wall top in the zone
89+
(`max(segment.start.z + segment.height)` over the zone walls), falling back to
90+
`2.0` m when the zone has no walls.
91+
92+
Ceilings are opaque from below and transparent from above. They are visual-only
93+
(no collision). With `ceiling_cast_shadows` false the ceiling does not occlude
94+
the sun, so interiors stay lit without global illumination.
7495

7596
Full field reference: [worlds/README.md](worlds/README.md).
7697

arena_simulation_setup/src/arena_simulation_setup/shared/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .entities import CustomDynamicObstacle, DynamicObstacle, Entity, Obstacle
44
from .walls import Wall
5-
from .world import Door, Elevator, Floor
5+
from .world import Ceiling, Door, Elevator, Floor
66

77
__all__ = [
88
"Pose",
@@ -14,6 +14,7 @@
1414
"CustomDynamicObstacle",
1515
"Wall",
1616
"Floor",
17+
"Ceiling",
1718
"Elevator",
1819
"Door",
1920
]

arena_simulation_setup/src/arena_simulation_setup/shared/world.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def _activation_distance_converter(x: float | typing.Sequence[float]) -> tuple[f
2121
@attrs.define
2222
class Elevator(Named):
2323
position: Position = attrs.field(converter=Position.converter)
24-
size: list[float] = attrs.field(factory=lambda: [2.0, 2.0, 2.5])
24+
size: list[float] = attrs.field(factory=lambda: [2.0, 2.0, 2.0])
2525
door_side: typing.Literal['+x', '-x', '+y', '-y'] = '+x'
2626
material: MaterialIdentifier = attrs.field(converter=MaterialIdentifier.converter, default=Material.default('elevator'))
2727
destination: str = attrs.field(default="")
@@ -77,3 +77,13 @@ class Floor(Named):
7777
x_length: float = attrs.field(converter=float, default=20.0)
7878
y_length: float = attrs.field(converter=float, default=20.0)
7979
material: MaterialIdentifier = attrs.field(converter=MaterialIdentifier.converter, default=Material.default('floor'))
80+
81+
82+
@attrs.define
83+
class Ceiling(Named):
84+
pos: Position = attrs.field(converter=Position.converter)
85+
x_length: float = attrs.field(converter=float, default=20.0)
86+
y_length: float = attrs.field(converter=float, default=20.0)
87+
z: float = attrs.field(converter=float, default=2.0)
88+
cast_shadows: bool = attrs.field(default=False)
89+
material: MaterialIdentifier = attrs.field(converter=MaterialIdentifier.converter, default=Material.default('ceiling'))

arena_simulation_setup/src/arena_simulation_setup/tree/World/World.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from arena_simulation_setup import ASS_DIR
1818
from arena_simulation_setup.shared import (
19+
Ceiling,
1920
Door,
2021
DynamicObstacle,
2122
Elevator,
@@ -67,6 +68,13 @@ class WorldEntities:
6768
doors: list[Door] = attrs.field(factory=list)
6869
elevators: list[Elevator] = attrs.field(factory=list)
6970
entities: WorldEntities = attrs.field(factory=WorldEntities)
71+
ceiling: bool = attrs.field(default=True)
72+
ceiling_height: float | None = attrs.field(default=None)
73+
ceiling_cast_shadows: bool = attrs.field(default=False)
74+
ceiling_material: MaterialIdentifier = attrs.field(
75+
converter=MaterialIdentifier.converter,
76+
default=Material.default('ceiling'),
77+
)
7078

7179
@property
7280
def floor(self) -> Floor:
@@ -97,6 +105,39 @@ def all_elevators(self) -> typing.Iterable[Elevator]:
97105
def all_floors(self) -> typing.Iterable[Floor]:
98106
return (zone.floor for zone in self.zones)
99107

108+
async def all_ceilings(self) -> list[Ceiling]:
109+
result: list[Ceiling] = []
110+
for zone in self.zones:
111+
if not zone.ceiling:
112+
continue
113+
if not zone.corners:
114+
continue
115+
x_min = min(corner.x for corner in zone.corners)
116+
y_min = min(corner.y for corner in zone.corners)
117+
x_max = max(corner.x for corner in zone.corners)
118+
y_max = max(corner.y for corner in zone.corners)
119+
pos = Position(x=(x_min + x_max) / 2, y=(y_min + y_max) / 2)
120+
x_length = x_max - x_min
121+
y_length = y_max - y_min
122+
if zone.ceiling_height is not None:
123+
z = zone.ceiling_height
124+
else:
125+
z = 2.0
126+
for wall in zone.walls:
127+
segments, _ = await wall.assets()
128+
for segment in segments:
129+
z = max(z, segment.start.z + segment.height)
130+
result.append(Ceiling(
131+
name=zone.name,
132+
pos=pos,
133+
x_length=x_length,
134+
y_length=y_length,
135+
z=z,
136+
cast_shadows=zone.ceiling_cast_shadows,
137+
material=zone.ceiling_material,
138+
))
139+
return result
140+
100141
@property
101142
def all_static_entities(self) -> typing.Iterable[Obstacle]:
102143
return (entity for zone in self.zones for entity in zone.entities.static)

arena_simulation_setup/src/arena_simulation_setup/tree/assets/Material.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,11 @@ class Material:
7171
'wall': MaterialIdentifier('Marble'),
7272
'floor': MaterialIdentifier('Porcelain_Tile_4'),
7373
'door': MaterialIdentifier('Aluminum_Anodized'),
74+
'ceiling': MaterialIdentifier('Concrete_Smooth'),
7475
}
7576

7677
@classmethod
77-
def default(cls, context: typing.Literal['floor', 'wall', 'door'] | str = '') -> MaterialIdentifier:
78+
def default(cls, context: typing.Literal['floor', 'wall', 'door', 'ceiling'] | str = '') -> MaterialIdentifier:
7879
return cls.__DEFAULTS.get(context, cls.__DEFAULT)
7980

8081
def asdict(self) -> dict:

0 commit comments

Comments
 (0)