In ProcGenKitchen
, objects are grouped into pseudo-atomic "arrangements". Each arrangement is procedurally generated. For example, a single kitchen counter with objects on top of the counter and inside the cabinet is generated via a KitchenCounter
arrangement.
Arrangement data classes are similar to add-ons in that they accept parameters and generate commands, but they are meant to be used within an add-on in groups. In ProcGenKitchen
, multiple kitchen counters, wall cabinets, etc. are placed alongside each other in the room.
Arrangements are procedurally generated. The models used are randomly selected from pre-categorized lists. Object positions, rotations, etc. are also randomized within various constraints.
Arrangements are all data objects. Every type of arrangement is a subclass of Arrangement
. Some arrangement types may have intermediary abstract subclasses such as ArrangementWithRootObject
.
There are many Arrangement
sub-classes; see the bottom of this document for a list. As adding example controller code to this document for every Arrangement
would be so verbose as to make this document unreadable, only a few notable examples would be shown. For others, you should read the API documentation.
A CupAndCoaster
is a subclass of Arrangement
. It creates either a cup or a wine glass. 50% of the time, there is a coaster under the cup or glass.
CupAndCoaster
has the following constructor parameters:
position
sets the position of the arrangement (either the coaster or, if there is no coaster, the cup or wineglass).rng
is optional and defaults to None. It is either a random seed (and integer) or anumpy.random.RandomState
object. If None, a newnumpy.random.RandomState
object is created.
This example controller adds a CupAndCoaster
to the scene:
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.image_capture import ImageCapture
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.proc_gen.arrangements.cup_and_coaster import CupAndCoaster
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
# Add a camera and enable image capture.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("cup_and_coaster")
print(f"Images will be saved to: {path}")
camera = ThirdPersonCamera(position={"x": -1.5, "y": 0.8, "z": 0},
look_at={"x": 0, "y": 0, "z": 0},
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Start the controller.
c = Controller()
c.add_ons.extend([camera, capture])
# Add a `CupAndCoaster` arrangement.
cup_and_coaster = CupAndCoaster(position={"x": 0, "y": 0, "z": 0},
rng=0)
# Create the scene.
commands = [TDWUtils.create_empty_room(12, 12)]
# Add commands to create the cup and coaster.
commands.extend(cup_and_coaster.get_commands())
# Send the commands.
c.communicate(commands)
c.communicate({"$type": "terminate"})
Result:
Arrangement.MODEL_CATEGORIES
is a dictionary of "proc-gen categories" and lists of model names. The categories and model names as a curated subset of models from models_core.json
. Proc-gen categories overlap with, but are not the same as, model_record.wcategory
.
This will print a list of all cups:
from tdw.proc_gen.arrangements.arrangement import Arrangement
for model_name in Arrangement.MODEL_CATEGORIES["cup"]:
print(model_name)
Output:
coffeecup004_fix
coffeemug
cup
An additional class variable, CupAndCoaster.CUP_CATEGORIES
, controls which categories are considered "cups" for the purposes of this particular arrangement:
from tdw.proc_gen.arrangements.cup_and_coaster import CupAndCoaster
for category in CupAndCoaster.CUP_CATEGORIES:
print(category)
Output:
cup
wineglass
To force CupAndCoaster
to use a specific cup or wineglass, we can set both MODEL_CATEGORIES
and CUP_CATEGORIES
:
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.image_capture import ImageCapture
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.proc_gen.arrangements.cup_and_coaster import CupAndCoaster
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
# Add a camera and enable image capture.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("cup_and_coaster")
print(f"Images will be saved to: {path}")
camera = ThirdPersonCamera(position={"x": -1.5, "y": 0.8, "z": 0},
look_at={"x": 0, "y": 0, "z": 0},
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Start the controller.
c = Controller()
c.add_ons.extend([camera, capture])
# Use only models from the category "cup".
CupAndCoaster.CUP_CATEGORIES = ["cup"]
# Use only the coffee mug model.
CupAndCoaster.MODEL_CATEGORIES["cup"] = ["coffeemug"]
# Add a `CupAndCoaster` arrangement.
cup_and_coaster = CupAndCoaster(position={"x": 0, "y": 0, "z": 0},
rng=0)
# Create the scene.
commands = [TDWUtils.create_empty_room(12, 12)]
# Add commands to create the cup and coaster.
commands.extend(cup_and_coaster.get_commands())
# Send the commands.
c.communicate(commands)
c.communicate({"$type": "terminate"})
Result:
Plate
is a subclass of ArrangementWithRootObject
. A plate may have food on top of it.
Plate
has the following constructor parameters:
position
sets the position of the root object (the plate).model
is optional and not shown in this example; set this to set the plate model. This can be either a string (a model name) or aModelRecord
.rng
is optional and defaults to None. It is either a random seed (and integer) or anumpy.random.RandomState
object. If None, a newnumpy.random.RandomState
object is created.
Additionally, Plate
has a field, root_object_id
, not found in Arrangement
(and, by extension, not found in CupAndCoaster
). ArrangementWithRootObject
subclasses all have a "root object" and any number of "child objects". root_object_id
is set after calling get_commands()
.
This example controller adds a Plate
to the scene. Note that in the CupAndCoaster
example, we added the add-ons, then the CupAndCoaster
, then called cup_and_coaster.get_commands()
. In this example, the order is flipped. This is because we want the camera to look at the plate; to do this, we need to set plate.root_object_id
which means that we need to call plate.get_commands()
before creating the ThirdPersonCamera
.
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.image_capture import ImageCapture
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.proc_gen.arrangements.plate import Plate
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
# Add a `Plate` arrangement.
plate = Plate(position={"x": 0, "y": 0, "z": 0},
rng=0)
plate_commands = plate.get_commands()
# The object ID of the plate is the root ID of the arrangement.
plate_id = plate.root_object_id
# Add a camera and enable image capture. Look at the plate.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("plate")
print(f"Images will be saved to: {path}")
camera = ThirdPersonCamera(position={"x": 0.5, "y": 0.2, "z": 0},
look_at=plate_id,
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Start the controller.
c = Controller()
c.add_ons.extend([camera, capture])
# Create the scene.
commands = [TDWUtils.create_empty_room(12, 12)]
# Add commands to create the arrangement.
commands.extend(plate_commands)
# Send the commands.
c.communicate(commands)
c.communicate({"$type": "terminate"})
Result:
Like the previous Plate
example, Microwave
is a subclass of ArrangementWithRootObject
. Unlike Plate
, Microwave
can actually create a sub-Arrangement
: in this case, it might add a Plate
arrangement inside the microwave. A Microwave
also has objects on top of it.
Microwave
has the following constructor parameters:
wall
is aCardinalDirection
. This controls the rotation of the microwave and the rest of the arrangment.position
sets the position of the root object (the microwave).model
is optional and can set the microwave model; it can be either a string (the model name) or aModelRecord
.rng
is optional and defaults to None. It is either a random seed (and integer) or anumpy.random.RandomState
object. If None, a newnumpy.random.RandomState
object is created.
All microwave models used by the Microwave
arrangement are composite objects with articulated doors.
This example controller adds a Microwave
to the scene. We'll add a CompositeObjectManager
and use it to open the microwave door so that we can see the Plate
arrangement. To open the door, we use some low-level API commands. In most controllers that use articulated objects, an agent would open the door instead.
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.image_capture import ImageCapture
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.add_ons.composite_object_manager import CompositeObjectManager
from tdw.proc_gen.arrangements.microwave import Microwave
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
from tdw.cardinal_direction import CardinalDirection
# Add a `Microwave` arrangement.
microwave = Microwave(position={"x": 0, "y": 0, "z": 0},
rng=2,
wall=CardinalDirection.west)
microwave_commands = microwave.get_commands()
# The object ID of the microwave is the root ID of the arrangement.
microwave_id = microwave.root_object_id
# Add a camera and enable image capture. Look at the microwave.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("microwave")
print(f"Images will be saved to: {path}")
camera = ThirdPersonCamera(position={"x": 1, "y": 0.8, "z": 0},
look_at=microwave_id,
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Add a composite object manager, which we'll use to open the microwave door.
composite_object_manager = CompositeObjectManager()
# Start the controller.
c = Controller()
c.add_ons.extend([camera, capture, composite_object_manager])
# Create the scene.
commands = [TDWUtils.create_empty_room(12, 12)]
# Add commands to create the arrangement.
commands.extend(microwave_commands)
# Send the commands.
c.communicate(commands)
# Start to open the door.
commands.clear()
for object_id in composite_object_manager.static:
if object_id == microwave_id:
for spring_id in composite_object_manager.static[object_id].springs:
commands.extend([{"$type": "set_spring_force",
"spring_force": 50,
"id": spring_id},
{"$type": "set_spring_target_position",
"target_position": 90,
"id": spring_id}])
break
c.communicate(commands)
# Open the door.
for i in range(50):
c.communicate([])
c.communicate({"$type": "terminate"})
Result:
ArrangementWithRootObject
(and, by extension, Microwave
as well as Plate
) has three dictionaries defining which single (non-arrangement) categories (not models) can be placed with respect to the root object. In this case, ON_TOP_OF["microwave"]
defines the categories of models that can be placed on the microwave. You can modify this list as needed, provided that the model is also listed somewhere in MODEL_CATEGORIES
(see above).
This example forces the Microwave
to add only bananas on top of it:
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.image_capture import ImageCapture
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.proc_gen.arrangements.microwave import Microwave
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
from tdw.cardinal_direction import CardinalDirection
# Add a `Microwave` arrangement.
microwave = Microwave(position={"x": 0, "y": 0, "z": 0},
rng=2,
wall=CardinalDirection.west)
# Only bananas can be on top of the microwave.
Microwave.ON_TOP_OF["microwave"] = ["banana"]
microwave_commands = microwave.get_commands()
# The object ID of the microwave is the root ID of the arrangement.
microwave_id = microwave.root_object_id
# Add a camera and enable image capture. Look at the microwave.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("microwave")
print(f"Images will be saved to: {path}")
camera = ThirdPersonCamera(position={"x": 1, "y": 0.8, "z": 0},
look_at=microwave_id,
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Start the controller.
c = Controller()
c.add_ons.extend([camera, capture])
# Create the scene.
commands = [TDWUtils.create_empty_room(12, 12)]
# Add commands to create the arrangement.
commands.extend(microwave_commands)
# Send the commands.
c.communicate(commands)
c.communicate({"$type": "terminate"})
Result:
KitchenCounter
is a subclass of KitchenCabinet
, which is a subclass of ArrangementAlongWall
, which is a subclass of ArrangementWithRootObject
, which is a subclass of Arrangement
.
A KitchenCounter
is positioned very differently than arrangements in previous examples in that it doesn't have an explicit position
constructor parameter. Instead, a KitchenCounter
is positioned in relationship to the room: a distance
from a corner
along a wall
.
A KitchenCounter
may have objects inside its cabinet (which has an articulated door). On the counter top, there will either be random single objects or a Microwave
arrangement (which in turn may have a Plate
and objects on top of it). A kitchen counter may also create a WallCabinet
above it, which in turn may have its own objects or sub-arrangements.
KitchenCounter
has the following constructor parameters:
cabinetry
defines the cabinetry set; this is used to make all kitchen cabinets, sinks, etc. in the scene look like they're part of the same set.cabinetry
is of typeCabinetry
. There are two pre-definedCabinetry
sets; seetdw.proc_gen.arrangements.cabinetry.cabinetry.CABINETRY
, a dictionary where the key is aCabinetryType
and the value is a pre-setCabinetry
.corner
is anOrdinalDirection
value describing the "starting corner" (this conceptually assumes that there are multiple kitchen counters, sinks, etc. along the wall, starting fromcorner
). The value ofcorner
must correspond to the value ofwall
; for example, ifwall == CardinalDirection.north
, then the two valid values forcorner
areOrdinalDirection.northwest
andOrdinalDirection.northeast
.wall
is aCardinalDirection
value describing the location of the wall that theKitchenCounter
abuts.distance
is a float describing the kitchen counter's distance from thecorner
along thewall
.region
is theInteriorRegion
that the wall, corner, and kitchen counter are located in. Read this for more information. In most cases, you can set this toscene_record.rooms[0].main_region
(assuming that you've already definedscene_record
).allow_microwave
is an optional boolean. If True, the kitchen counter may have a microwave. This can be useful for controlling the total number of microwaves in a scene. It is optional and defaults to True.cabinet_is_empty_probability
is an optional float defining the probability of the kitchen cabinet being empty.model
is either a string (the name of a model) or aModelRecord
. This is the root kitchen counter model.wall_length
is the length of the wall. If None, it defaults to the actual length of the wall. This can be useful if you want to start calculating thedistance
at an offset.rng
is optional and defaults to None. It is either a random seed (and integer) or anumpy.random.RandomState
object. If None, a newnumpy.random.RandomState
object is created.
This example adds a kitchen counter to the scene:
from tdw.controller import Controller
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.add_ons.image_capture import ImageCapture
from tdw.proc_gen.arrangements.kitchen_counter import KitchenCounter
from tdw.proc_gen.arrangements.cabinetry.cabinetry import CABINETRY
from tdw.proc_gen.arrangements.cabinetry.cabinetry_type import CabinetryType
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
from tdw.ordinal_direction import OrdinalDirection
from tdw.cardinal_direction import CardinalDirection
from tdw.librarian import SceneLibrarian
# Get the scene name, record, and the region where the kitchen counter will be added.
scene_name = "mm_craftroom_2a"
scene_record = SceneLibrarian().get_record("mm_craftroom_2a")
region = scene_record.rooms[0].main_region
# Generate a kitchen counter.
kitchen_counter = KitchenCounter(cabinetry=CABINETRY[CabinetryType.beech_honey],
corner=OrdinalDirection.northeast,
wall=CardinalDirection.north,
distance=0,
region=region,
rng=3)
# Add a camera and enable image capture.
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("kitchen_counter")
print(f"Images will be saved to: {path}")
# Look at the root object (the kitchen counter).
camera = ThirdPersonCamera(position={"x": 0, "y": 1.8, "z": 0},
look_at=kitchen_counter.root_object_id,
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
# Start the controller.
c = Controller()
# Append the add-ons.
c.add_ons = [camera, capture]
# Add the scene.
commands = [Controller.get_add_scene(scene_name=scene_name)]
# Add the kitchen counter's commands.
commands.extend(kitchen_counter.get_commands())
c.communicate(commands)
c.communicate({"$type": "terminate"})
Result:
Unlike nearly all data classes on TDW, Arrangement
subclasses are structured such that most of their parameters are in class variables rather than constructor parameters. This can be awkward to code when adding arrangements directly to a scene but it's very useful when using an add-on such as ProcGenKitchen
. In ProcGenKitchen
, it's impossible to know beforehand which objects will be placed where (unless you're using a known random seed), and so it's impossible to set constructor parameter values per-object. You can, however, set class variable values, which will affect all arrangements of a given type.
To learn more about the class variables, read the relevant Arrangement API document.
In this example, this controller sets Basket.ROTATION
, which controls the maximum random rotation of all baskets in the scene:
from tdw.controller import Controller
from tdw.add_ons.proc_gen_kitchen import ProcGenKitchen
from tdw.add_ons.third_person_camera import ThirdPersonCamera
from tdw.add_ons.image_capture import ImageCapture
from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
from tdw.proc_gen.arrangements.basket import Basket
Basket.ROTATION = 5
path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("proc_gen_kitchen_minimal")
print(f"Images will be saved to: {path}")
proc_gen_kitchen = ProcGenKitchen()
proc_gen_kitchen.create(rng=0)
camera = ThirdPersonCamera(position={"x": 2, "y": 1.8, "z": -0.5},
look_at={"x": 0, "y": 0.6, "z": 0},
avatar_id="a")
capture = ImageCapture(avatar_ids=["a"], path=path, pass_masks=["_img"])
c = Controller()
c.add_ons.extend([proc_gen_kitchen, camera, capture])
c.communicate([])
c.communicate({"$type": "terminate"})
Many arrangements add secondary objects using a "rectangular arrangement" algorithm. In this algorithm, a rectangular area with a given (x, y, z) center position is defined and then divided into "cells". Each cell may have an object on it and objects may span multiple cells. The position of the objects are perturbed slightly and the rotations of the objects are typically random. Each cell may also be empty.
KitchenCounter
, for example, has two rectangular arrangements: one on the counter top, and one enclosed by the cabinet door(s).
KitchenCounter.COUNTER_TOP_CELL_SIZE
andKitchenCounter.CABINET_CELL_SIZE
control the cell size of the rectangular arrangement. A lower value allows for smaller objects.KitchenCounter.COUNTER_TOP_CELL_DENSITY
andKitchenCounter.CABINET_CELL_DENSITY
control how often cells are empty. A lower value means more objects.KitchenCounter.COUNTER_TOP_DEPTH_SCALE
,KitchenCounter.COUNTER_TOP_WIDTH_SCALE
,KitchenCounter.CABINET_DEPTH_SCALE
, andKitchenCounter.CABINET_WIDTH_SCALE
control the spatial padding of the rectangular arrangement to prevent objects from being added too close to the edges.KitchenCounter.ON_TOP_OF["kitchen_counter"]
sets the categories of objects that can be placed on the counter top.KitchenCounter.ENCLOSED_BY["kitchen_counter"]
sets the categories of objects that can be placed inside the cabinet.
You can adjust parameters like this to increase or decrease the number of objects, types of objects, etc. on root object surfaces. For a more "cluttered" look, try decreasing the cell size, increasing the cell density, and adding more categories to ON_TOP_OF
or ENCLOSED_BY
.
Next: Scripted object placement (floorplan layouts)
Example Controllers:
- cup_and_coaster.py Create a cup and coaster.
- plate.py Create a plate arrangement.
- microwave.py Create a microwave arrangement.
- kitchen_counter.py Create a kitchen counter arrangement.
Python API:
- Add-ons:
- Arrangements:
- Cabinetry:
- Directions:
- Scene Data: