Exemple #1
0
class RubeGoldbergDemo(Controller):

    def __init__(self):
        self.obj_name_dict: Dict[int, str] = {}
        
        # Set up the object transform data.
        object_setup_data = json.loads(Path("object_setup.json").read_text())
        self.object_setups: List[_ObjectSetup] = []
        for o in object_setup_data:
            combo = _ObjectSetup(id=int(o),
                                 model_name=object_setup_data[o]["model_name"],
                                 position=object_setup_data[o]["position"],
                                 rotation=object_setup_data[o]["rotation"],
                                 scale=object_setup_data[o]["scale"])
            self.object_setups.append(combo)

        # Parse the default objects.csv spreadsheet. 
        self.object_audio_data = PyImpact.get_object_info()

        self.temp_amp = 0.1

        # Keep track of the current trial number, for logging purposes.
        self.current_trial_num = 0

        # Fetch the ball and board model's records; we will need them later to change its material.
        self.special_models = ModelLibrarian(library="models_special.json")
        self.full_models = ModelLibrarian(library="models_full.json")
        self.ball_record = self.special_models.get_record("prim_sphere")
        self.board_record = self.full_models.get_record("wood_board")

        # Set path to write out logging info.
        self.root_dest_dir = Path("dist/mode_properties_logs")
        if not self.root_dest_dir.exists():
            self.root_dest_dir.mkdir(parents=True)

        super().__init__()
 
 
    def run(self, num_trials: int):
        """
        Build a "Rube Goldberg" machine to produce impact sounds.
        """

        # Load the photorealistic "archviz_house" environment.
        self.load_streamed_scene(scene="archviz_house_2018")

        # Organize all initialization commands into a single list.
        # Set global values, including the desired screen size and aspect ratio (720P).
        # Frame rate is set to 60 fps to facilitate screen / video recording.
        init_commands = [{"$type": "set_render_quality",
                          "render_quality": 5},
                         {"$type": "set_screen_size",
                          "width": 1280,
                          "height": 720},
                         {"$type": "set_target_framerate",
                          "framerate": 60},
                         {"$type": "set_time_step",
                          "time_step": 0.02}]

        # Create the avatar. 
        init_commands.extend(TDWUtils.create_avatar(avatar_type="A_Img_Caps_Kinematic",
                                                    position={"x": -15.57, "y": 1.886, "z": -4.97}))

        # Aim the avatar camera to frame the desired view.
        init_commands.extend([{"$type": "rotate_sensor_container_by",
                               "axis": "yaw",
                               "angle": 109.13,
                               "sensor_name": "SensorContainer",
                               "avatar_id": "a"},
                              {"$type": "rotate_sensor_container_by",
                               "axis": "pitch",
                               "angle": 6.36,
                               "sensor_name": "SensorContainer",
                               "avatar_id": "a"}])

        # Add the audio sensor.
        init_commands.extend([{"$type": "add_audio_sensor",
                               "avatar_id": "a"}])

        # Adjust post-processing settings.
        init_commands.extend([{"$type": "set_post_exposure",
                               "post_exposure": 0.35},
                             {"$type": "set_screen_space_reflections",
                              "enabled": True},
                             {"$type": "set_vignette",
                              "enabled": False},
                             {"$type": "set_ambient_occlusion_intensity",
                              "intensity": 0.175},
                             {"$type": "set_ambient_occlusion_thickness_modifier",
                              "thickness": 5.0}])

        # Set the shadow strength to maximum.
        init_commands.extend([{"$type": "set_shadow_strength",
                               "strength": 1.0}])

        # Send all of the initialization commands.
        self.communicate(init_commands)

        for i in range(num_trials):
            self.do_trial()
            

    def do_trial(self):
        # Initialize PyImpact and pass in the "master gain" amplitude value. This value must be betweem 0 and 1.
        # The relative amplitudes of all scene objects involved in collisions will be scaled relative to this value.
        # Note -- if this value is too high, waveform clipping can occur and the resultant audio will be distorted.
        # For this reason, the value used here is considerably smaller than the corresponding value used in the
        # impact_sounds.py example controller. Here we have a large number of closely-occuring collisions resulting in
        # a rapid series of "clustered" impact sounds, as opposed to a single object falling from a height;
        # using a higher value such as the 0.5 used in the example controller will definitely result in unpleasant
        # distortion of the audio.
        # Note that logging is also enabled.

        # Keep track of trial number.
        self.current_trial_num += 1

        # Create folder for this trial's logging info.
        dest_dir = self.root_dest_dir.joinpath(str(self.current_trial_num))
        if not dest_dir.exists():
            dest_dir.mkdir(parents=True)
        dest_dir_str = str(dest_dir.resolve())

        p = PyImpact(0.25, logging=True)

        self.add_all_objects()

        # "Aim" the ball at the monkey and apply the force.
        # Note that this force value was arrived at through a number of trial-and-error iterations.
        resp = self.communicate([{"$type": "object_look_at_position",
                                  "id": 0,
                                  "position": {"x": -12.95, "y": 1.591, "z": -5.1}},
                                 {"$type": "apply_force_magnitude_to_object",
                                  "id": 0,
                                  "magnitude": 98.0}])

        for i in range(400):
            collisions, environment_collision, rigidbodies = PyImpact.get_collisions(resp)
            # Sort the objects by mass.
            masses: Dict[int, float] = {}
            for j in range(rigidbodies.get_num()):
                masses.update({rigidbodies.get_id(j): rigidbodies.get_mass(j)})

            # If there was a collision, create an impact sound.
            if len(collisions) > 0 and PyImpact.is_valid_collision(collisions[0]):
                collider_id = collisions[0].get_collider_id()
                collidee_id = collisions[0].get_collidee_id()
                # The "target" object should have less mass than the "other" object.
                if masses[collider_id] < masses[collidee_id]:
                    target_id = collider_id
                    other_id = collidee_id
                else:
                    target_id = collidee_id
                    other_id = collider_id

                target_name = self.obj_name_dict[target_id]
                other_name = self.obj_name_dict[other_id]

                impact_sound_command = p.get_impact_sound_command(
                    collision=collisions[0],
                    rigidbodies=rigidbodies,
                    target_id=target_id,
                    target_mat=self.object_audio_data[target_name].material,
                    other_id=other_id,
                    other_mat=self.object_audio_data[other_name].material,
                    target_amp=self.object_audio_data[target_name].amp,
                    other_amp=self.object_audio_data[other_name].amp,
                    resonance=self.object_audio_data[target_name].resonance)
                resp = self.communicate(impact_sound_command)
            # Continue to run the trial.
            else:
                resp = self.communicate([])

        # Get the logging info for this trial and write it out.
        log = p.get_log()
        json_dest = dest_dir.joinpath("mode_properties_log.json")
        json_dest.write_text(json.dumps(log, indent=2))

        for obj_setup in self.object_setups:
            self.communicate({"$type": "destroy_object", "id": obj_setup.id})

		

    def add_all_objects(self):
        object_commands = []
        rigidbodies = []

        # Set the mass and scale of the objects, from  the data files we parsed earlier.
        # Enable output of collision and rigid body data.
        # Build dictionary of id,,name so we can retrieve object names during collisions.
        # Cache the ids for objects we need to change materials for later
        for obj_setup in self.object_setups:
            self.obj_name_dict[obj_setup.id] = obj_setup.model_name
            rigidbodies.append(obj_setup.id)
            if obj_setup.model_name == "prim_sphere":
                ball_id = obj_setup.id
            elif obj_setup.model_name == "102_pepsi_can_12_fl_oz_vray":
                coke_can_id = obj_setup.id
            elif obj_setup.model_name == "wood_board":
                board_id = obj_setup.id
            elif obj_setup.model_name == "bench":
                table_id = obj_setup.id
            elif obj_setup.model_name == "camera_box":
                camera_box_id = obj_setup.id

            # Set up to add all objects
            object_commands.extend([self.get_add_object(
                                    model_name=obj_setup.model_name, 
                                    object_id=obj_setup.id,
                                    position=obj_setup.position,
                                    rotation=obj_setup.rotation,
                                    library=self.object_audio_data[obj_setup.model_name].library),
                                    {"$type": "set_mass",
                                    "id": obj_setup.id,
                                    "mass": self.object_audio_data[obj_setup.model_name].mass},
                                    {"$type": "set_object_collision_detection_mode", 
                                    "id": obj_setup.id, 
                                    "mode": "continuous_speculative"},
                                    {"$type": "scale_object", 
                                     "id":  obj_setup.id, 
                                     "scale_factor": obj_setup.scale}])

        object_commands.extend([{"$type": "send_collisions",
                                 "enter": True,
                                 "stay": False,
                                 "exit": False},
                                {"$type": "send_rigidbodies",
                                 "frequency": "always",
                                 "ids": rigidbodies}])

        # Scale the ball and set a suitable drag value to "tune" how hard it will hit the monkey.
        # This combined with the force applied below hits the monkey just hard enough to set the sequnce of
        # collisions in motion.
        object_commands.extend([{"$type": "scale_object",
                                 "id": ball_id,
                                 "scale_factor": {"x": 0.1, "y": 0.1, "z": 0.1}},
                                {"$type": "set_object_drag",
                                 "angular_drag": 5.0,
                                 "drag": 1.0,
                                 "id": ball_id}])

        # Set physics material parameters to enable rolling motion for the coke can and board after being hit,
        # so they collide with the row of "dominos"
        object_commands.extend([{"$type": "set_physic_material",
                                 "dynamic_friction": 0.4,
                                 "static_friction": 0.4,
                                 "bounciness": 0.6,
                                 "id": table_id},
                                {"$type": "set_physic_material",
                                 "dynamic_friction": 0.2,
                                 "static_friction": 0.2,
                                 "bounciness": 0.6,
                                 "id": coke_can_id},
                                {"$type": "set_physic_material",
                                 "dynamic_friction": 0.2,
                                 "static_friction": 0.2,
                                 "bounciness": 0.7,
                                 "id": board_id}])

        # Set the camera box and table to be kinematic, we don't need them to respond to physics.
        object_commands.extend([{"$type": "set_kinematic_state",
                                 "id": table_id,
                                 "is_kinematic": True},
                                {"$type": "set_kinematic_state",
                                 "id": camera_box_id,
                                 "is_kinematic": True}])

        # Set the visual material of the ball to metal and the board to a different wood than the bench.
        object_commands.extend(TDWUtils.set_visual_material(self, self.ball_record.substructure, ball_id,
                                                            "dmd_metallic_fine", quality="high"))
        object_commands.extend(TDWUtils.set_visual_material(self, self.board_record.substructure, board_id,
                                                            "wood_tropical_hardwood", quality="high"))

        # Send all of the object setup commands.
        self.communicate(object_commands)
Exemple #2
0
from pathlib import Path
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.librarian import ModelLibrarian
from tdw.output_data import OutputData, Bounds, Images
"""
1. Add a table and place an object on the table.
2. Add a camera and receive an image.
"""

lib = ModelLibrarian("models_core.json")
# Get the record for the table.
table_record = lib.get_record("small_table_green_marble")

c = Controller()

table_id = 0

# 1. Load the scene.
# 2. Create an empty room (using a wrapper function)
# 3. Add the table.
# 4. Request Bounds data.
resp = c.communicate([{
    "$type": "load_scene",
    "scene_name": "ProcGenScene"
},
                      TDWUtils.create_empty_room(12, 12),
                      c.get_add_object(model_name=table_record.name,
                                       object_id=table_id,
                                       position={
                                           "x": 0,
Exemple #3
0
class Controller(object):
    """
    Base class for all controllers.

    Usage:

    ```python
    from tdw.controller import Controller
    c = Controller()
    c.start()
    ```
    """
    def __init__(self,
                 port: int = 1071,
                 check_version: bool = True,
                 launch_build: bool = True):
        """
        Create the network socket and bind the socket to the port.

        :param port: The port number.
        :param check_version: If true, the controller will check the version of the build and print the result.
        :param launch_build: If True, automatically launch the build. If one doesn't exist, download and extract the correct version. Set this to False to use your own build, or (if you are a backend developer) to use Unity Editor.
        """

        # Compare the installed version of the tdw Python module to the latest on PyPi.
        # If there is a difference, recommend an upgrade.
        if check_version:
            self._check_pypi_version()

        # Launch the build.
        if launch_build:
            Controller.launch_build()

        context = zmq.Context()

        self.socket = context.socket(zmq.REP)
        self.socket.bind('tcp://*:' + str(port))

        self.socket.recv()

        self.model_librarian: Optional[ModelLibrarian] = None
        self.scene_librarian: Optional[SceneLibrarian] = None
        self.material_librarian: Optional[MaterialLibrarian] = None
        self.hdri_skybox_librarian: Optional[HDRISkyboxLibrarian] = None
        self.humanoid_librarian: Optional[HumanoidLibrarian] = None
        self.humanoid_animation_librarian: Optional[
            HumanoidAnimationLibrarian] = None

        # Compare the version of the tdw module to the build version.
        if check_version and launch_build:
            self._check_build_version()

    def communicate(self, commands: Union[dict, List[dict]]) -> list:
        """
        Send commands and receive output data in response.

        :param commands: A list of JSON commands.

        :return The output data from the build.
        """

        if not isinstance(commands, list):
            commands = [commands]

        self.socket.send_multipart([json.dumps(commands).encode('utf-8')])

        return self.socket.recv_multipart()

    def start(self, scene="ProcGenScene") -> None:
        """
        Init TDW.

        :param scene: The scene to load.
        """

        self.communicate([{"$type": "load_scene", "scene_name": scene}])

    def get_add_object(self,
                       model_name: str,
                       object_id: int,
                       position={
                           "x": 0,
                           "y": 0,
                           "z": 0
                       },
                       rotation={
                           "x": 0,
                           "y": 0,
                           "z": 0
                       },
                       library: str = "") -> dict:
        """
        Returns a valid add_object command.

        :param model_name: The name of the model.
        :param position: The position of the model.
        :param rotation: The starting rotation of the model, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `ModelLibrarian.get_library_filenames()` and `ModelLibrarian.get_default_library()`.
        :param object_id: The ID of the new object.

        :return An add_object command that the controller can then send.
        """

        if self.model_librarian is None or (
                library != "" and self.model_librarian.library != library):
            self.model_librarian = ModelLibrarian(library=library)

        record = self.model_librarian.get_record(model_name)

        return {
            "$type": "add_object",
            "name": model_name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": position,
            "rotation": rotation,
            "category": record.wcategory,
            "id": object_id
        }

    def get_add_material(self, material_name: str, library: str = "") -> dict:
        """
        Returns a valid add_material command.

        :param material_name: The name of the material.
        :param library: The path to the records file. If left empty, the default library will be selected. See `MaterialLibrarian.get_library_filenames()` and `MaterialLibrarian.get_default_library()`.

        :return An add_material command that the controller can then send.
        """

        if self.material_librarian is None:
            self.material_librarian = MaterialLibrarian(library=library)

        record = self.material_librarian.get_record(material_name)
        return {
            "$type": "add_material",
            "name": material_name,
            "url": record.get_url()
        }

    def get_add_scene(self, scene_name: str, library: str = "") -> dict:
        """
        Returns a valid add_scene command.

        :param scene_name: The name of the scene.
        :param library: The path to the records file. If left empty, the default library will be selected. See `SceneLibrarian.get_library_filenames()` and `SceneLibrarian.get_default_library()`.

        :return An add_scene command that the controller can then send.
        """

        if self.scene_librarian is None:
            self.scene_librarian = SceneLibrarian(library=library)

        record = self.scene_librarian.get_record(scene_name)
        return {
            "$type": "add_scene",
            "name": scene_name,
            "url": record.get_url()
        }

    def get_add_hdri_skybox(self, skybox_name: str, library: str = "") -> dict:
        """
        Returns a valid add_hdri_skybox command.

        :param skybox_name: The name of the skybox.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HDRISkyboxLibrarian.get_library_filenames()` and `HDRISkyboxLibrarian.get_default_library()`.

        :return An add_hdri_skybox command that the controller can then send.
        """

        if self.hdri_skybox_librarian is None:
            self.hdri_skybox_librarian = HDRISkyboxLibrarian(library=library)

        record = self.hdri_skybox_librarian.get_record(skybox_name)
        return {
            "$type": "add_hdri_skybox",
            "name": skybox_name,
            "url": record.get_url(),
            "exposure": record.exposure,
            "initial_skybox_rotation": record.initial_skybox_rotation,
            "sun_elevation": record.sun_elevation,
            "sun_initial_angle": record.sun_initial_angle,
            "sun_intensity": record.sun_intensity
        }

    def get_add_humanoid(self,
                         humanoid_name: str,
                         object_id: int,
                         position={
                             "x": 0,
                             "y": 0,
                             "z": 0
                         },
                         rotation={
                             "x": 0,
                             "y": 0,
                             "z": 0
                         },
                         library: str = "") -> dict:
        """
        Returns a valid add_humanoid command.

        :param humanoid_name: The name of the humanoid.
        :param position: The position of the humanoid.
        :param rotation: The starting rotation of the humanoid, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HumanoidLibrarian.get_library_filenames()` and `HumanoidLibrarian.get_default_library()`.
        :param object_id: The ID of the new object.

        :return An add_humanoid command that the controller can then send.
        """

        if self.humanoid_librarian is None or (
                library != "" and self.humanoid_librarian.library != library):
            self.humanoid_librarian = HumanoidLibrarian(library=library)

        record = self.humanoid_librarian.get_record(humanoid_name)

        return {
            "$type": "add_humanoid",
            "name": humanoid_name,
            "url": record.get_url(),
            "position": position,
            "rotation": rotation,
            "id": object_id
        }

    def get_add_humanoid_animation(
            self,
            humanoid_animation_name: str,
            library="") -> (dict, HumanoidAnimationRecord):
        """
        Returns a valid add_humanoid_animation command and the record (which you will need to play an animation).

        :param humanoid_animation_name: The name of the animation.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HumanoidAnimationLibrarian.get_library_filenames()` and `HumanoidAnimationLibrarian.get_default_library()`.

        return An add_humanoid_animation command that the controller can then send.
        """

        if self.humanoid_animation_librarian is None:
            self.humanoid_animation_librarian = HumanoidAnimationLibrarian(
                library=library)

        record = self.humanoid_animation_librarian.get_record(
            humanoid_animation_name)
        return {
            "$type": "add_humanoid_animation",
            "name": humanoid_animation_name,
            "url": record.get_url()
        }, record

    def load_streamed_scene(self, scene="tdw_room_2018") -> None:
        """
        Load a streamed scene. This is equivalent to: `c.communicate(c.get_add_scene(scene))`

        :param scene: The name of the streamed scene.
        """

        self.communicate(self.get_add_scene(scene))

    def add_object(self,
                   model_name: str,
                   position={
                       "x": 0,
                       "y": 0,
                       "z": 0
                   },
                   rotation={
                       "x": 0,
                       "y": 0,
                       "z": 0
                   },
                   library: str = "") -> int:
        """
        Add a model to the scene. This is equivalent to: `c.communicate(c.get_add_object())`

        :param model_name: The name of the model.
        :param position: The position of the model.
        :param rotation: The starting rotation of the model, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `ModelLibrarian.get_library_filenames()` and `ModelLibrarian.get_default_library()`.

        :return The ID of the new object.
        """

        object_id = Controller.get_unique_id()
        self.communicate(
            self.get_add_object(model_name, object_id, position, rotation,
                                library))

        return object_id

    def get_version(self) -> Tuple[str, str]:
        """
        Send a send_version command to the build.

        :return The TDW version and the Unity Engine version.
        """

        resp = self.communicate({"$type": "send_version"})
        for r in resp[:-1]:
            if Version.get_data_type_id(r) == "vers":
                v = Version(r)
                return v.get_tdw_version(), v.get_unity_version()
        if len(resp) == 1:
            raise Exception(
                "Tried receiving version output data but didn't receive anything!"
            )
        raise Exception(f"Expected output data with ID vers but got: " +
                        Version.get_data_type_id(resp[0]))

    @staticmethod
    def get_unique_id() -> int:
        """
        Generate a unique integer. Useful when creating objects.

        :return The new unique ID.
        """

        return int.from_bytes(os.urandom(3), byteorder='big')

    @staticmethod
    def get_frame(frame: bytes) -> int:
        """
        Converts the frame byte array to an integer.

        :param frame: The frame as bytes.

        :return The frame as an integer.
        """

        return int.from_bytes(frame, byteorder='big')

    @staticmethod
    def launch_build() -> None:
        """
        Launch the build. If a build doesn't exist at the expected location, download one to that location.
        """

        # Download the build.
        if not Build.BUILD_PATH.exists():
            print(
                f"Couldn't find build at {Build.BUILD_PATH}\nDownloading now..."
            )
            success = Build.download()
            if not success:
                print("You need to launch your own build.")
        else:
            success = True
        # Launch the build.
        if success:
            Popen(str(Build.BUILD_PATH.resolve()))

    def _check_build_version(self,
                             version: str = __version__,
                             build_version: str = None) -> None:
        """
        Check the version of the build. If there is no build, download it.
        If the build is of the wrong version, recommend an upgrade.


        :param version: The version of TDW. You can set this to an arbitrary version for testing purposes.
        :param build_version: If not None, this overrides the expected build version. Only override for debugging.
        """

        v = PyPi.strip_post_release(version)
        tdw_version, unity_version = self.get_version()
        # Override the build version for testing.
        if build_version is not None:
            tdw_version = build_version
        pypi_version = PyPi.get_latest_minor_release(tdw_version)
        print(
            f"Build version {tdw_version}\nUnity Engine {unity_version}\nPython tdw module version {version}"
        )
        if v < tdw_version:
            print(
                "WARNING! Your TDW build is newer than your tdw Python module. They might not be compatible."
            )
            print(
                f"To download the correct build:\n\nfrom tdw.release.build import Build\nBuild.download(version={v})"
            )
            print(
                f"\nTo upgrade your Python module (usually recommended):\n\npip3 install tdw=={pypi_version}"
            )
        elif v > tdw_version:
            print(
                "WARNING! Your TDW build is older than your tdw Python module. Downloading the correct build..."
            )
            Build.download(v)

    @staticmethod
    def _check_pypi_version(v_installed_override: str = None,
                            v_pypi_override: str = None) -> None:
        """
        Compare the version of the tdw Python module to the latest on PyPi.
        If there is a mismatch, offer an upgrade recommendation.

        :param v_installed_override: Override for the installed version. Change this to debug.
        :param v_pypi_override: Override for the PyPi version. Change this to debug.
        """

        # Get the version of the installed tdw module.
        installed_tdw_version = PyPi.get_installed_tdw_version()
        # Get the latest version of the tdw module on PyPi.
        pypi_version = PyPi.get_pypi_version()

        # Apply overrides
        if v_installed_override is not None:
            installed_tdw_version = v_installed_override
        if v_pypi_override is not None:
            pypi_version = v_pypi_override

        # If there is a mismatch, recommend an upgrade.
        if installed_tdw_version != pypi_version:
            # Strip the installed version of the post-release suffix (e.g. 1.6.3.4 to 1.6.3).
            stripped_installed_version = PyPi.strip_post_release(
                installed_tdw_version)
            # This message is here only for debugging.
            if stripped_installed_version != __version__:
                print(
                    f"Your installed version: {stripped_installed_version} "
                    f"doesn't match tdw.version.__version__: {__version__} "
                    f"(this may be because you're using code from the tdw repo that is ahead of PyPi)."
                )
            # Strip the latest PyPi version of the post-release suffix.
            stripped_pypi_version = PyPi.strip_post_release(pypi_version)
            print(
                f"You are using TDW {installed_tdw_version} but version {pypi_version} is available."
            )

            # If user is behind by a post release, recommend an upgrade to the latest.
            # (Example: installed version is 1.6.3.4 and PyPi version is 1.6.3.5)
            if stripped_installed_version == stripped_pypi_version:
                print(
                    f"Upgrade to the latest version of TDW:\npip3 install tdw -U"
                )

            # Using a version behind the latest (e.g. latest is 1.6.3 and installed is 1.6.2)
            # If the user is behind by a major or minor release, recommend either upgrading to a minor release
            # or to a major release.
            # (Example: installed version is 1.6.3.4 and PyPi version is 1.7.0.0)
            else:
                installed_major = PyPi.get_major_release(
                    stripped_installed_version)
                pypi_minor = PyPi.get_latest_minor_release(
                    stripped_installed_version)
                # Minor release mis-match.
                if PyPi.strip_post_release(
                        pypi_minor) != stripped_installed_version:
                    print(
                        f"To upgrade to the last version of 1.{installed_major}:\n"
                        f"pip3 install tdw=={pypi_minor}")
                pypi_major = PyPi.get_major_release(stripped_pypi_version)
                # Major release mis-match.
                if installed_major != pypi_major:
                    # Offer to upgrade to a major release.
                    print(
                        f"Consider upgrading to the latest version of TDW ({stripped_pypi_version}):"
                        f"\npip3 install tdw -U")
        else:
            print("Your installed tdw Python module is up to date with PyPi.")
class Submerge(FlexDataset):
    """
    Create a fluid "container" with the NVIDIA Flex physics engine.
    Run several trials, dropping ball objects of increasing mass into the fluid.
    """
    def __init__(self):
        self.model_list = [
            "b03_db_apps_tech_08_04", "trashbin", "trunck",
            "whirlpool_akzm7630ix", "satiro_sculpture", "towel-radiator-2",
            "b03_folding-screen-panel-room-divider",
            "naughtone_pinch_stool_chair", "microwave", "trunk_6810-0009",
            "suitcase", "kayak_small", "elephant_bowl", "trapezoidal_table",
            "b05_pc-computer-printer-1", "dishwasher_4",
            "chista_slice_of_teak_table", "buddah", "b05_elsafe_infinity_ii",
            "backpack", "b06_firehydrant_lod0", "b05_ticketmachine",
            "b05_trophy", "b05_kitchen_aid_toster", "b05_heavybag",
            "bongo_drum_hr_blend", "b03_worldglobe", "ceramic_pot",
            "b04_kenmore_refr_70419", "b03_zebra", "b05_gibson_j-45",
            "b03_cow", "b03_sheep", "b04_stringer"
        ]

        # Cache the record for the receptacle.
        self.receptacle_record = ModelLibrarian(
            "models_special.json").get_record("fluid_receptacle1x1")
        # Cache the fluid types.
        self.ft = FluidTypes()

        self.full_lib = ModelLibrarian("models_full.json")

        self.pool_id = None

        super().__init__()

    def get_scene_initialization_commands(self) -> List[dict]:
        if system() != "Windows":
            raise Exception(
                "Flex fluids are only supported in Windows (see Documentation/misc_frontend/flex.md)"
            )

        commands = [
            self.get_add_scene(scene_name="tdw_room_2018"), {
                "$type": "set_aperture",
                "aperture": 4.8
            }, {
                "$type": "set_focus_distance",
                "focus_distance": 2.25
            }, {
                "$type": "set_post_exposure",
                "post_exposure": 0.4
            }, {
                "$type": "set_ambient_occlusion_intensity",
                "intensity": 0.175
            }, {
                "$type": "set_ambient_occlusion_thickness_modifier",
                "thickness": 3.5
            }
        ]

        return commands

    def get_trial_initialization_commands(self) -> List[dict]:
        super().get_trial_initialization_commands()

        trial_commands = []
        if self.pool_id is not None:
            trial_commands.append({"$type": "destroy_flex_container", "id": 0})

        # Load a pool container for the fluid.
        self.pool_id = Controller.get_unique_id()
        self.non_flex_objects.append(self.pool_id)
        trial_commands.append(
            self.add_transforms_object(record=self.receptacle_record,
                                       position={
                                           "x": 0,
                                           "y": 0,
                                           "z": 0
                                       },
                                       rotation={
                                           "x": 0,
                                           "y": 0,
                                           "z": 0
                                       },
                                       o_id=self.pool_id))
        trial_commands.extend([{
            "$type": "scale_object",
            "id": self.pool_id,
            "scale_factor": {
                "x": 1.5,
                "y": 2.5,
                "z": 1.5
            }
        }, {
            "$type": "set_kinematic_state",
            "id": self.pool_id,
            "is_kinematic": True,
            "use_gravity": False
        }])

        # Randomly select a fluid type
        fluid_type_selection = choice(self.ft.fluid_type_names)

        # Create the container, set up for fluids.
        # Slow down physics so the water can settle without splashing out of the container.
        trial_commands.extend([{
            "$type":
            "create_flex_container",
            "collision_distance":
            0.04,
            "static_friction":
            0.1,
            "dynamic_friction":
            0.1,
            "particle_friction":
            0.1,
            "viscocity":
            self.ft.fluid_types[fluid_type_selection].viscosity,
            "adhesion":
            self.ft.fluid_types[fluid_type_selection].adhesion,
            "cohesion":
            self.ft.fluid_types[fluid_type_selection].cohesion,
            "radius":
            0.1,
            "fluid_rest":
            0.05,
            "damping":
            0.01,
            "substep_count":
            5,
            "iteration_count":
            8,
            "buoyancy":
            1.0
        }, {
            "$type": "set_time_step",
            "time_step": 0.005
        }])

        # Recreate fluid.
        # Add the fluid actor, using the FluidPrimitive. Allow 500 frames for the fluid to settle before continuing.
        fluid_id = Controller.get_unique_id()
        trial_commands.extend(
            self.add_fluid_object(position={
                "x": 0,
                "y": 1.0,
                "z": 0
            },
                                  rotation={
                                      "x": 0,
                                      "y": 0,
                                      "z": 0
                                  },
                                  o_id=fluid_id,
                                  fluid_type=fluid_type_selection))

        trial_commands.append({"$type": "set_time_step", "time_step": 0.03})

        # Select an object at random.
        model = choice(self.model_list)
        model_record = self.full_lib.get_record(model)
        info = PHYSICS_INFO[model]

        # Randomly select an object, and randomly orient it.
        # Set the solid actor and assign the container.
        o_id = Controller.get_unique_id()
        trial_commands.extend(
            self.add_solid_object(record=model_record,
                                  position={
                                      "x": 0,
                                      "y": 2,
                                      "z": 0
                                  },
                                  rotation={
                                      "x": uniform(-45.0, 45.0),
                                      "y": uniform(-45.0, 45.0),
                                      "z": uniform(-45.0, 45.0)
                                  },
                                  o_id=o_id,
                                  scale={
                                      "x": 0.5,
                                      "y": 0.5,
                                      "z": 0.5
                                  },
                                  mass_scale=info.mass,
                                  particle_spacing=0.05))
        # Reset physics time-step to a more normal value.
        # Position and aim avatar.
        trial_commands.extend([{
            "$type": "set_kinematic_state",
            "id": o_id
        }, {
            "$type": "teleport_avatar_to",
            "position": {
                "x": -2.675,
                "y": 1.375,
                "z": 0
            }
        }, {
            "$type": "look_at",
            "object_id": self.pool_id,
            "use_centroid": True
        }])

        return trial_commands

    def get_per_frame_commands(self, frame: int,
                               resp: List[bytes]) -> List[dict]:
        return [{"$type": "focus_on_object", "object_id": self.pool_id}]

    def get_field_of_view(self) -> float:
        return 35

    def is_done(self, resp: List[bytes], frame: int) -> bool:
        return frame > 200
    src_paths = a.prefab_to_asset_bundle(
        Path.home().joinpath("asset_bundle_creator/Assets/Resources/prefab"),
        model_name=args.name)
    src_asset_bundles = dict()
    for q in src_paths:
        src_asset_bundles[q.parts[-2]] = q

    # Parse the URLs.
    urls = a.get_local_urls(src_paths)

    # Create the metadata record.
    record_path = a.create_record(args.name, 2886585, "container", 1, urls)
    record = ModelRecord(json.loads(record_path.read_text(encoding="utf-8")))

    # Add the record.
    r = lib.get_record(record.name)
    lib.add_or_update_record(record=record,
                             overwrite=False if r is None else True,
                             write=True)
    # Make the URLs relative paths.
    temp = dict()
    for p in record.urls:
        dest_dir = f"../asset_bundles/{p}"
        dd = root_dest.joinpath(f"asset_bundles/{p}")
        if not dd.exists():
            dd.mkdir(parents=True)
        temp[p] = f"../asset_bundles/{p}/{record.name}"
    record.urls = temp
    lib.add_or_update_record(record=record, overwrite=True, write=True)
    # Copy the asset bundles.
    for p in UNITY_TO_SYSTEM:
Exemple #6
0
class Controller(object):
    """
    Base class for all controllers.

    Usage:

    ```python
    from tdw.controller import Controller
    c = Controller()
    c.start()
    ```
    """
    def __init__(self, port: int = 1071, check_version: bool = True):
        """
        Create the network socket and bind the socket to the port.

        :param port: The port number.
        :param check_version: If true, the controller will check the version of the build and print the result.
        """

        context = zmq.Context()

        self.socket = context.socket(zmq.REP)
        self.socket.bind('tcp://*:' + str(port))

        self.socket.recv()

        self.model_librarian: Optional[ModelLibrarian] = None
        self.scene_librarian: Optional[SceneLibrarian] = None
        self.material_librarian: Optional[MaterialLibrarian] = None
        self.hdri_skybox_librarian: Optional[HDRISkyboxLibrarian] = None
        self.humanoid_librarian: Optional[HumanoidLibrarian] = None
        self.humanoid_animation_librarian: Optional[
            HumanoidAnimationLibrarian] = None

        # Compare the version of the tdw module to the build version.
        if check_version:
            tdw_version, unity_version = self.get_version()
            print(
                f"Build version {tdw_version}\nUnity Engine {unity_version}\nPython tdw module version {__version__}"
            )
            if __version__ != tdw_version:
                print(
                    "WARNING! Your Python code is not the same version as the build. They might not be compatible."
                )
                print(
                    "Either use the latest prerelease build, or use the last stable release via:\n"
                )
                print("git checkout v" + last_stable_release)

    def communicate(self, commands: Union[dict, List[dict]]) -> list:
        """
        Send commands and receive output data in response.

        :param commands: A list of JSON commands.

        :return The output data from the build.
        """

        if not isinstance(commands, list):
            commands = [commands]

        self.socket.send_multipart([json.dumps(commands).encode('utf-8')])

        return self.socket.recv_multipart()

    def start(self, scene="ProcGenScene") -> None:
        """
        Init TDW.

        :param scene: The scene to load.
        """

        self.communicate([{"$type": "load_scene", "scene_name": scene}])

    def get_add_object(self,
                       model_name: str,
                       object_id: int,
                       position={
                           "x": 0,
                           "y": 0,
                           "z": 0
                       },
                       rotation={
                           "x": 0,
                           "y": 0,
                           "z": 0
                       },
                       library: str = "") -> dict:
        """
        Returns a valid add_object command.

        :param model_name: The name of the model.
        :param position: The position of the model.
        :param rotation: The starting rotation of the model, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `ModelLibrarian.get_library_filenames()` and `ModelLibrarian.get_default_library()`.
        :param object_id: The ID of the new object.

        :return An add_object command that the controller can then send.
        """

        if self.model_librarian is None or (
                library != "" and self.model_librarian.library != library):
            self.model_librarian = ModelLibrarian(library=library)

        record = self.model_librarian.get_record(model_name)

        return {
            "$type": "add_object",
            "name": model_name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": position,
            "rotation": rotation,
            "category": record.wcategory,
            "id": object_id
        }

    def get_add_material(self, material_name: str, library: str = "") -> dict:
        """
        Returns a valid add_material command.

        :param material_name: The name of the material.
        :param library: The path to the records file. If left empty, the default library will be selected. See `MaterialLibrarian.get_library_filenames()` and `MaterialLibrarian.get_default_library()`.

        :return An add_material command that the controller can then send.
        """

        if self.material_librarian is None:
            self.material_librarian = MaterialLibrarian(library=library)

        record = self.material_librarian.get_record(material_name)
        return {
            "$type": "add_material",
            "name": material_name,
            "url": record.get_url()
        }

    def get_add_scene(self, scene_name: str, library: str = "") -> dict:
        """
        Returns a valid add_scene command.

        :param scene_name: The name of the scene.
        :param library: The path to the records file. If left empty, the default library will be selected. See `SceneLibrarian.get_library_filenames()` and `SceneLibrarian.get_default_library()`.

        :return An add_scene command that the controller can then send.
        """

        if self.scene_librarian is None:
            self.scene_librarian = SceneLibrarian(library=library)

        record = self.scene_librarian.get_record(scene_name)
        return {
            "$type": "add_scene",
            "name": scene_name,
            "url": record.get_url()
        }

    def get_add_hdri_skybox(self, skybox_name: str, library: str = "") -> dict:
        """
        Returns a valid add_hdri_skybox command.

        :param skybox_name: The name of the skybox.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HDRISkyboxLibrarian.get_library_filenames()` and `HDRISkyboxLibrarian.get_default_library()`.

        :return An add_hdri_skybox command that the controller can then send.
        """

        if self.hdri_skybox_librarian is None:
            self.hdri_skybox_librarian = HDRISkyboxLibrarian(library=library)

        record = self.hdri_skybox_librarian.get_record(skybox_name)
        return {
            "$type": "add_hdri_skybox",
            "name": skybox_name,
            "url": record.get_url(),
            "exposure": record.exposure,
            "initial_skybox_rotation": record.initial_skybox_rotation,
            "sun_elevation": record.sun_elevation,
            "sun_initial_angle": record.sun_initial_angle,
            "sun_intensity": record.sun_intensity
        }

    def get_add_humanoid(self,
                         humanoid_name: str,
                         object_id: int,
                         position={
                             "x": 0,
                             "y": 0,
                             "z": 0
                         },
                         rotation={
                             "x": 0,
                             "y": 0,
                             "z": 0
                         },
                         library: str = "") -> dict:
        """
        Returns a valid add_humanoid command.

        :param humanoid_name: The name of the humanoid.
        :param position: The position of the humanoid.
        :param rotation: The starting rotation of the humanoid, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HumanoidLibrarian.get_library_filenames()` and `HumanoidLibrarian.get_default_library()`.
        :param object_id: The ID of the new object.

        :return An add_humanoid command that the controller can then send.
        """

        if self.humanoid_librarian is None or (
                library != "" and self.humanoid_librarian.library != library):
            self.humanoid_librarian = HumanoidLibrarian(library=library)

        record = self.humanoid_librarian.get_record(humanoid_name)

        return {
            "$type": "add_humanoid",
            "name": humanoid_name,
            "url": record.get_url(),
            "position": position,
            "rotation": rotation,
            "id": object_id
        }

    def get_add_humanoid_animation(
            self,
            humanoid_animation_name: str,
            library="") -> (dict, HumanoidAnimationRecord):
        """
        Returns a valid add_humanoid_animation command and the record (which you will need to play an animation).

        :param humanoid_animation_name: The name of the animation.
        :param library: The path to the records file. If left empty, the default library will be selected. See `HumanoidAnimationLibrarian.get_library_filenames()` and `HumanoidAnimationLibrarian.get_default_library()`.

        return An add_humanoid_animation command that the controller can then send.
        """

        if self.humanoid_animation_librarian is None:
            self.humanoid_animation_librarian = HumanoidAnimationLibrarian(
                library=library)

        record = self.humanoid_animation_librarian.get_record(
            humanoid_animation_name)
        return {
            "$type": "add_humanoid_animation",
            "name": humanoid_animation_name,
            "url": record.get_url()
        }, record

    def load_streamed_scene(self, scene="tdw_room_2018") -> None:
        """
        Load a streamed scene. This is equivalent to: `c.communicate(c.get_add_scene(scene))`

        :param scene: The name of the streamed scene.
        """

        self.communicate(self.get_add_scene(scene))

    def add_object(self,
                   model_name: str,
                   position={
                       "x": 0,
                       "y": 0,
                       "z": 0
                   },
                   rotation={
                       "x": 0,
                       "y": 0,
                       "z": 0
                   },
                   library: str = "") -> int:
        """
        Add a model to the scene. This is equivalent to: `c.communicate(c.get_add_object())`

        :param model_name: The name of the model.
        :param position: The position of the model.
        :param rotation: The starting rotation of the model, in Euler angles.
        :param library: The path to the records file. If left empty, the default library will be selected. See `ModelLibrarian.get_library_filenames()` and `ModelLibrarian.get_default_library()`.

        :return The ID of the new object.
        """

        object_id = Controller.get_unique_id()
        self.communicate(
            self.get_add_object(model_name, object_id, position, rotation,
                                library))

        return object_id

    def get_version(self) -> Tuple[str, str]:
        """
        Send a send_version command to the build.

        :return The TDW version and the Unity Engine version.
        """

        resp = self.communicate({"$type": "send_version"})
        for r in resp[:-1]:
            if Version.get_data_type_id(r) == "vers":
                v = Version(r)
                return v.get_tdw_version(), v.get_unity_version()
        if len(resp) == 1:
            raise Exception(
                "Tried receiving version output data but didn't receive anything!"
            )
        raise Exception(f"Expected output data with ID vers but got: " +
                        Version.get_data_type_id(resp[0]))

    @staticmethod
    def get_unique_id() -> int:
        """
        Generate a unique integer. Useful when creating objects.

        :return The new unique ID.
        """

        return int.from_bytes(os.urandom(3), byteorder='big')

    @staticmethod
    def get_frame(frame: bytes) -> int:
        """
        Converts the frame byte array to an integer.

        :param frame: The frame as bytes.

        :return The frame as an integer.
        """

        return int.from_bytes(frame, byteorder='big')