diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index a3283fe2b..ed99f6f1b 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -5,11 +5,11 @@ on:
pull_request:
branches:
- main
- - develop
+ - develop*
push:
branches:
- main
- - develop
+ - develop*
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -22,7 +22,7 @@ env:
jobs:
checks:
-
+ if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
@@ -32,7 +32,7 @@ jobs:
run: |
pip install ruff
# stop the build if there are Python syntax errors or undefined names
- ruff --output-format=github --select=E9,F63,F7,F82 .
+ ruff check --output-format=github --select=E9,F63,F7,F82 .
# default set of ruff rules with GitHub Annotations
#ruff --format=github . # to be enabled in a future PR
@@ -47,4 +47,4 @@ jobs:
- name: Test with pytest
run: |
- pytest tests -m ci
\ No newline at end of file
+ pytest tests -k 'not skip_for_ci'
\ No newline at end of file
diff --git a/README.md b/README.md
index 9d9e88ce1..acc877e87 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,68 @@
+
+

+
-## [Infinigen: Infinite Photorealistic Worlds using Procedural Generation](https://infinigen.org)
+# [Infinigen: Infinite Photorealistic Worlds Using Procedural Generation](https://infinigen.org)
-Please visit our website, [https://infinigen.org](https://infinigen.org)
+[**Getting Started**](#getting-started)
+| [**Website**](https://infinigen.org/)
+| [**Intro Video**](https://www.youtube.com/watch?v=6tgspeI-GHY)
+| [**Papers**](#papers)
+| [**Documentation**](#documentation)
+| [**Contributing**](#contributing)
-[](https://youtu.be/6tgspeI-GHY)
+
+

+
-If you use Infinigen in your work, please cite our [academic paper]([https://arxiv.org/abs/2306.09310](https://arxiv.org/abs/2306.09310)):
+## Getting Started
+
+First, follow our [Installation Instructions](docs/Installation.md).
+
+### Hello Room: Getting Started with Infinigen Indoors
+
+
+
+
+
+
+
+
+See instructions & example commands for Infinigen-Indoors in [HelloRoom.md](docs/HelloRoom.md)
+
+### Hello World: Getting Started with Infinigen Nature
+
+
+
+
+
+
+
+
+See instructions & example commands for Infinigen-Nature in [HelloWorld.md](docs/HelloWorld.md)
+
+## Papers
+
+If you use Infinigen in your work, please cite our academic papers:
-Alexander Raistrick*, Lahav Lipson*, Zeyu Ma* (*equal contribution, alphabetical order)
-Lingjie Mei, Mingzhe Wang, Yiming Zuo, Karhan Kayan, Hongyu Wen, Beining Han,
-Yihan Wang, Alejandro Newell, Hei Law, Ankit Goyal, Kaiyu Yang, Jia Deng
+Alexander Raistrick*,
+Lahav Lipson*,
+Zeyu Ma* (*equal contribution, alphabetical order)
+Lingjie Mei,
+Mingzhe Wang,
+Yiming Zuo,
+Karhan Kayan,
+Hongyu Wen,
+Beining Han,
+Yihan Wang,
+Alejandro Newell,
+Hei Law,
+Ankit Goyal,
+Kaiyu Yang,
+Jia Deng
Conference on Computer Vision and Pattern Recognition (CVPR) 2023
@@ -26,23 +76,39 @@ Conference on Computer Vision and Pattern Recognition (CVPR) 2023
}
```
-### Getting Started
-
-First, follow our [Installation Instructions](docs/Installation.md).
-
-Next, see our ["Hello World" example](docs/HelloWorld.md) to generate an image & ground truth similar to those shown below.
-
+
-
-
-
-
+Alexander Raistrick*,
+Lingjie Mei*,
+Karhan Kayan*, (*equal contribution, random order)
+David Yan,
+Yiming Zuo,
+Beining Han,
+Hongyu Wen,
+Meenal Parakh,
+Stamatis Alexandropoulos,
+Lahav Lipson,
+Zeyu Ma,
+Jia Deng
+Conference on Computer Vision and Pattern Recognition (CVPR) 2024
-### Documentation
+```
+@inproceedings{infinigen2024indoors,
+ author = {Raistrick, Alexander and Mei, Lingjie and Kayan, Karhan and Yan, David and Zuo, Yiming and Han, Beining and Wen, Hongyu and Parakh, Meenal and Alexandropoulos, Stamatis and Lipson, Lahav and Ma, Zeyu and Deng, Jia},
+ title = {Infinigen Indoors: Photorealistic Indoor Scenes using Procedural Generation},
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
+ month = {June},
+ year = {2024},
+ pages = {21783-21794}
+}
+```
+
+## Documentation
- [Installation Guide](docs/Installation.md)
-- ["Hello World": Generate your first Infinigen scene](docs/HelloWorld.md)
+- ["Hello World": Generate your first Infinigen-Nature scene](docs/HelloWorld.md)
+- ["Hello Room": Generate your first Infinigen-Indoors scene](docs/HelloRoom.md)
- [Configuring Infinigen](docs/ConfiguringInfinigen.md)
- [Downloading pre-generated data](docs/PreGeneratedData.md)
- [Generating individual assets](docs/GeneratingIndividualAssets.md)
@@ -51,10 +117,10 @@ Next, see our ["Hello World" example](docs/HelloWorld.md) to generate an image &
- [Implementing new materials & assets](docs/ImplementingAssets.md)
- [Generating fluid simulations](docs/GeneratingFluidSimulations.md)
-### Coming Soon
Please see our [project roadmap](https://infinigen.org/roadmap) and follow us at [https://twitter.com/PrincetonVL](https://twitter.com/PrincetonVL) for updates.
-### Contributing
+## Contributing
+
We welcome contributions! You can contribute in many ways:
- **Contribute code to this repository** - We welcome code contributions. More guidelines coming soon.
- **Contribute procedural generators** - `infinigen/nodes/node_transpiler/dev_script.py` provides tools to convert artist-friendly [Blender Nodes](https://docs.blender.org/manual/en/2.79/render/blender_render/materials/nodes/introduction.html) into python code. Tutorials and guidelines coming soon.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index d72471b20..7b5d3fd15 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -51,3 +51,8 @@ v1.3.3
v1.3.4
- Fixed bug where individual export would fail on objects hidden from viewport
- Fixed Terrain.populated_bounds bad merge
+
+v1.4.0 - Infinigen Indoors
+- Add library of procedural generators for indoor objects & materials
+- Add indoor scene generation system, including constraint language and solver
+- Add HelloRoom.md & ExportingToSimulators.md
diff --git a/docs/ConfiguringInfinigen.md b/docs/ConfiguringInfinigen.md
index e739b1f58..1acc24d04 100644
--- a/docs/ConfiguringInfinigen.md
+++ b/docs/ConfiguringInfinigen.md
@@ -33,13 +33,13 @@ Infinigen is designed to run many independent scenes in paralell. This means tha
Both `manage_jobs.py` and `infinigen_examples/generate_nature.py` can be configured via the commandline or config files, using [Google's "Gin Config"](https://github.com/google/gin-config). Gin allows you to insert new default keyword arguments ("kwargs") for any function decorated with `@gin.configurable`; many such functions exist in our codebase, and via gin overrides you can create datsets suiting many diverse applications, as is explained in the coming sections.
-To use gin, simply add commandline arguments such as `-p compose_scene.rain_particles_chance = 1.0` to override the chance of rain, or `--pipeline_overrides iterate_scene_tasks.frame_range=[1,25]` to set a video's length to 24 frames. You can chain many statements together, separated by spaces, to configure many parts of the system at once. These statements depend on knowing the python names of the function and keyword argument you wish to override. To find parameters you wish to override, you should browse `infinigen_examples/configs/base.gin` and other configs, or `infinigen_examples/generate_nature.py` and the definitions of any functions it calls. Better documentation and organization of the available parameters will come in future versions.
+To use gin, simply add commandline arguments such as `-p compose_nature.rain_particles_chance = 1.0` to override the chance of rain, or `--pipeline_overrides iterate_scene_tasks.frame_range=[1,25]` to set a video's length to 24 frames. You can chain many statements together, separated by spaces, to configure many parts of the system at once. These statements depend on knowing the python names of the function and keyword argument you wish to override. To find parameters you wish to override, you should browse `infinigen_examples/configs_nature/base.gin` and other configs, or `infinigen_examples/generate_nature.py` and the definitions of any functions it calls. Better documentation and organization of the available parameters will come in future versions.
If you find a useful and related combination of these commandline overrides, you can write them into a `.gin` file in `infinigen_examples/configs`. Then, to load that config, just include the name of the file into the `--configs`. If your overrides target `manage_jobs` rather than `infinigen_examples/generate_nature.py` you should place the config file in `datagen/configs` and use `--pipeline_configs` rather than `--configs`.
-Our `infinigen_examples/generate_nature.py` driver always loads [`infinigen_examples/configs/base.gin`][../infinigen_examples/configs/base.gin], and you can inspect / modify this file to see many common and useful gin override options.
+Our `infinigen_examples/generate_nature.py` driver always loads [`infinigen_examples/configs_nature/base.gin`][../infinigen_examples/configs_nature/base.gin], and you can inspect / modify this file to see many common and useful gin override options.
-`infinigen_examples/generate_nature.py` also expects that one file from (configs/scene_types/)[infinigen_examples/configs/scene_types] will be loaded. These scene_type configs contain gin overrides designed to encode the semantic constraints of real natural habitats (e.g. `infinigen_examples/configs/scene_types/desert.gin` causes sand to appear and cacti to be more likely).
+`infinigen_examples/generate_nature.py` also expects that one file from (configs/scene_types/)[infinigen_examples/configs_nature/scene_types] will be loaded. These scene_type configs contain gin overrides designed to encode the semantic constraints of real natural habitats (e.g. `infinigen_examples/configs_nature/scene_types/desert.gin` causes sand to appear and cacti to be more likely).
### Moving beyond "Hello World"
@@ -57,7 +57,7 @@ Here is a breakdown of what every commandline argument does, and ideas for how y
- `--specific_seed 0` forces the system to use a random seed of your choice, rather than choosing one at random. Change this seed to get a different random variation, or remove it to have the program choose a seed at random
- `--num_scenes` decides how many unique scenes the program will attempt to generate before terminating. Once you have removed `--specific_seed`, you can increase this to generate many scenes in sequence or in paralell.
- `--configs desert.gin simple.gin` forces the command to generate a desert scene, and to do so with relatively low mesh detail, low render resolution, low render samples, and some asset types disabled.
- - Do `--configs snowy_mountain.gin simple.gin` to try out a different scene type (`snowy_mountain.gin` can instead be any scene_type option from `infinigen_examples/configs/scene_types/`)
+ - Do `--configs snowy_mountain.gin simple.gin` to try out a different scene type (`snowy_mountain.gin` can instead be any scene_type option from `infinigen_examples/configs_nature/scene_types/`)
- Remove the `desert.gin` and just specify `--configs simple.gin` to use random scene types according to the weighted list in `datagen/configs/base.gin`.
- You have the option of removing `simple.gin` and specify neither of the original configs. This turns off the many detail-reduction options included in `simple.gin`, and will create scenes closer to those in our intro video, albeit at significant compute costs. Removing `simple.gin` will likely cause crashes unless using a workstation/server with large amounts of RAM and VRAM. You can find more details on optimizing scene content for performance [here](#config-overrides-for-mesh-detail-and-performance).
- `--pipeline_configs local_16GB.gin monocular.gin blender_gt.gin`
@@ -106,22 +106,22 @@ Generating a video, stereo or other dataset typically requires more render jobs,
To create longer videos, modify `iterate_scene_tasks.frame_range` in `monocular_video.gin` (note: we use 24fps video by default). `iterate_scene_tasks.view_block_size` controls how many frames will be grouped into each `fine_terrain` and render / ground-truth task.
-If you need more than two cameras, or want to customize their placement, see `infinigen_examples/configs/base.gin`'s `camera.spawn_camera_rigs.camera_rig_config` for advice on existing options, or write your own code to instantiate a custom camera setup.
+If you need more than two cameras, or want to customize their placement, see `infinigen_examples/configs_nature/base.gin`'s `camera.spawn_camera_rigs.camera_rig_config` for advice on existing options, or write your own code to instantiate a custom camera setup.
### Config Overrides to Customize Scene Content
:bulb: If you only care about few specific assets, or want to export Infinigen assets to another project, instead see [Generating individual assets](GeneratingIndividualAssets.md).
-You can achieve a great deal of customization by browsing and editing `infinigen_examples/configs/base.gin` - e.g. modifying cameras, lighting, asset placement, etc.
+You can achieve a great deal of customization by browsing and editing `infinigen_examples/configs_nature/base.gin` - e.g. modifying cameras, lighting, asset placement, etc.
- `base.gin` only provides the default values of these configs, and may be overridden by scene_type configs. To apply a setting globally across all scene types, you should put them in a new config placed at the end of your `--configs` argument (so that it's overrides are applied last), or use commandline overrides.
However, many options exist which are not present in base.gin. At present, you must browse `infinigen_examples/generate_nature.py` to find the part of the code you wish to customize, and look through the relevant code for what more advanced @gin.configurable functions are available. You can also add @gin.configurable to most functions to allow additional configuration. More documentation on available parameters is coming soon.
-For most steps of `infinigen_examples/generate_nature.py`'s `compose_scene` function, we use our `RandomStageExecutor` wrapper to decide whether the stage is run, and handle other bookkeeping. This means that if you want to decide the probability with which some asset is included in a scene, you can use the gin override `compose_scene.trees_chance=1.0` or something similar depending on the name string provided as the first argument of the relevant run_stage calls in this way, e.g. `compose_scene.rain_particles_chance=0.9`to make most scenes rainy, or `compose_scene.flowers_chance=0.1` to make flowers rarer.
+For most steps of `infinigen_examples/generate_nature.py`'s `compose_nature` function, we use our `RandomStageExecutor` wrapper to decide whether the stage is run, and handle other bookkeeping. This means that if you want to decide the probability with which some asset is included in a scene, you can use the gin override `compose_nature.trees_chance=1.0` or something similar depending on the name string provided as the first argument of the relevant run_stage calls in this way, e.g. `compose_nature.rain_particles_chance=0.9`to make most scenes rainy, or `compose_nature.flowers_chance=0.1` to make flowers rarer.
-A common request is to just turn off things you don't want to see, which can be achieved by adding `compose_scene.trees_chance=0.0` or similar to your `-p` argument or a loaded config file. To conveniently turn off lots of things at the same time, we provide configs in `infinigen_examples/configs/disable_assets` to disable things like all creatures, or all particles.
+A common request is to just turn off things you don't want to see, which can be achieved by adding `compose_nature.trees_chance=0.0` or similar to your `-p` argument or a loaded config file. To conveniently turn off lots of things at the same time, we provide configs in `infinigen_examples/configs_nature/disable_assets` to disable things like all creatures, or all particles.
-You will also encounter configs using what we term a "registry pattern", e.g. `infinigen_examples/configs/base_surface_registry.gin`'s `ground_collection`. "Registries", in this project, are a list of discrete generators, with weights indicating how relatively likely they are to be chosen each time the registry is sampled.
+You will also encounter configs using what we term a "registry pattern", e.g. `infinigen_examples/configs_nature/base_surface_registry.gin`'s `ground_collection`. "Registries", in this project, are a list of discrete generators, with weights indicating how relatively likely they are to be chosen each time the registry is sampled.
- For example, in `base_surface_registry.gin`, `surface.registry.beach` specifies `("sand", 10)` to indicate that sand has high weight to be chosen to be assigned for the beach category.
- Weights are normalized by their overall sum to obtain a probability distribution.
- Name strings undergo lookup in the relevant source code folders, e.g. the name "sand" in a surface registry maps to `infinigen/assets/materials/sand.py`.
@@ -131,27 +131,27 @@ You will also encounter configs using what we term a "registry pattern", e.g. `i
The quantity, diversity and detail of assets in a scene drastically affects RAM/VRAM requirements and runtime. This section will highlight configurable parameters that may help tune Infinigen to run better on limited hardware, or that could be increased to create larger more detailed scenes.
Infinigen can generate meshes at a wide range of resolutions. Many gin-configurable options exist to customize how detailed our meshes will be.
These options, as well as the choice of scene type configs, are the most effective ways to decrease compute costs.
-- All mesh resolutions are defined in terms of pixels-per-face. Increasing/decreasing the `compose_scene.generate_resolution` will have corresponding effects on geometry detail. If you wish to render the same mesh at a different resolution, override `render_image.render_resolution_override`
+- All mesh resolutions are defined in terms of pixels-per-face. Increasing/decreasing the `compose_nature.generate_resolution` will have corresponding effects on geometry detail. If you wish to render the same mesh at a different resolution, override `render_image.render_resolution_override`
- `OpaqueSphericalMesher.pixels_per_cube` and `TransparentSphericalMesher.pixels_per_cube`. You can increase them from their default, 2 pixels per marching cube, to 4 (as seen in `dev.gin`) or higher, in order to reduce terrain resolution and cost. Low resolution terrain will cause noticeable artifacts in videos, you must use `high_quality_terrain.gin` if rendering videos with moving cameras.
- `target_face_size.global_multiplier` controls the resolution of all other assets. Increase it to 4 or higher to reduce the compute cost of non-terrain assets like plants and creatures.
- All of these options have diminishing returns - a minimal amount of geometry or data is always generated to help define the shape of each asset, which may be larger than the final geometry if you set resolution very low.
Infinigen curbs memory costs by only populating assets up to a certain distance away from the camera (except for terrain, which is essentially unbounded). `base.gin` contains many options to customize how far away, and thus how many, assets will be placed:
- `placement.populate_all.dist_cull` controls the maximum distance to populate placeholders (for assets such as trees, cacti, creatures, etc). Reducing this will curb the number of assets and especially the number of trees, which can be quite expensive.
- - `compose_scene.inview_distance`control the maximum distance to scatter plants/rocks/medium-sized-objects. Reducing this will reduce the number of poses for these assets, but will not reduce the number of unique underlying meshes, so may have diminishing returns.
- - Similarly to the above, `compose_scene.near_distance` controls the maximum distance to scatter tiny particles like pine needles.
+ - `compose_nature.inview_distance`control the maximum distance to scatter plants/rocks/medium-sized-objects. Reducing this will reduce the number of poses for these assets, but will not reduce the number of unique underlying meshes, so may have diminishing returns.
+ - Similarly to the above, `compose_nature.near_distance` controls the maximum distance to scatter tiny particles like pine needles.
- Infinigen does not populate assets which are far outside the camera frustrum. You may attempt to reduce camera FOV to minimize how many assets are in view, but be warned there will be minimal or significantly diminishing returns on performance, due to the need to keep out-of-view assets loaded to retain accurate lighting/shadows.
-We also provide `infinigen_examples/configs/performance/dev.gin`, a config which sets many of the above performance parameters to achieve lower scenes. We often use this config to obtain previews for development purposes, but it may also be suitable for generating lower resolution images/scenes for some tasks.
+We also provide `infinigen_examples/configs_nature/performance/dev.gin`, a config which sets many of the above performance parameters to achieve lower scenes. We often use this config to obtain previews for development purposes, but it may also be suitable for generating lower resolution images/scenes for some tasks.
Our current system determines asset mesh resolution based on the _closest distance_ it comes to the camera during an entire trajectory. Therefore, longer videos are more expensive, as more assets will be closer to the camera at some point in the trajectory. Options exist to re-generate assets at new resolutions over the course of a video to curb these costs - please make a Github Issue for advice.
If you find yourself bottlenecked by GPU time, you should consider the following options:
- Render single images or stereo images, rather than video, such that less render jobs are required for each CPU-bound `coarse`/`populate` job
- - Reduce `base.gin`'s `full/render_image.num_samples = 8192` or `compose_scene.generate_resolution = (1920, 1080)`. This proportionally reduces rendering FLOPS, with some diminishing returns due to BVH setup time.
+ - Reduce `base.gin`'s `full/render_image.num_samples = 8192` or `compose_nature.generate_resolution = (1920, 1080)`. This proportionally reduces rendering FLOPS, with some diminishing returns due to BVH setup time.
- If your GPU(s) are _underutilized_, try the reverse of these tips.
-Some scene type configs are also generally more expensive than others. `forest.gin` and `coral.gin` are very expensive due to dense detailed fauna, wheras `artic` and `snowy_mountain` are very cheap. Low-resource compute settings (<64GB) of RAM may only be able to handle a subset of our `infinigen_examples/configs/scene_type/` options, and you may wish to tune the ratios of scene_types by editing `datagen/configs/base.gin` or otherwise overriding `sample_scene_spec.config_distribution`.
+Some scene type configs are also generally more expensive than others. `forest.gin` and `coral.gin` are very expensive due to dense detailed fauna, wheras `artic` and `snowy_mountain` are very cheap. Low-resource compute settings (<64GB) of RAM may only be able to handle a subset of our `infinigen_examples/configs_nature/scene_type/` options, and you may wish to tune the ratios of scene_types by editing `datagen/configs/base.gin` or otherwise overriding `sample_scene_spec.config_distribution`.
### Other `manage_jobs.py` commandline options
@@ -200,7 +200,7 @@ These commands are intended as inspiration - please read docs above for more adv
python -m infinigen.datagen.manage_jobs --output_folder outputs/my_videos --num_scenes 500 \
--pipeline_config slurm monocular cuda_terrain opengl_gt \
--cleanup big_files --warmup_sec 30000 \
- --overrides compose_scene.rain_particles_chance=1.0
+ --overrides compose_nature.rain_particles_chance=1.0
```
:bulb: You can substitute the `rain_particles` in `rain_particles_chance` for any `run_stage` name argument string in `infinigen_examples/generate_nature.py`, such as `trees` or `ground_creatures`.
@@ -211,7 +211,7 @@ python -m infinigen.datagen.manage_jobs --output_folder outputs/my_videos --num_
--pipeline_config slurm monocular cuda_terrain opengl_gt \
--cleanup big_files --warmup_sec 30000 --config no_assets
```
-:bulb: You can substitute "no_assets" for `no_creatures` or `no_particles`, or the name of any file under `infinigen_examples/configs`. The command shown uses `infinigen_examples/configs/disable_assets/no_assets.gin`.
+:bulb: You can substitute "no_assets" for `no_creatures` or `no_particles`, or the name of any file under `infinigen_examples/configs`. The command shown uses `infinigen_examples/configs_nature/disable_assets/no_assets.gin`.
Create videos at birds-eye-view camera altitudes:
@@ -222,7 +222,7 @@ python -m infinigen.datagen.manage_jobs --output_folder outputs/my_videos --num_
--overrides camera.camera_pose_proposal.altitude=["uniform", 20, 30]
```
-:bulb: The command shown is overriding `infinigen_examples/configs/base.gin`'s default setting of `camera.camera_pose_proposal.altitude`. You can use a similar syntax to override any number of .gin config entries. Separate multiple entries with spaces.
+:bulb: The command shown is overriding `infinigen_examples/configs_nature/base.gin`'s default setting of `camera.camera_pose_proposal.altitude`. You can use a similar syntax to override any number of .gin config entries. Separate multiple entries with spaces.
Create 1 second video clips:
```
diff --git a/docs/ExportingToExternalFileFormats.md b/docs/ExportingToExternalFileFormats.md
index 9fdb128d2..1268e3b07 100644
--- a/docs/ExportingToExternalFileFormats.md
+++ b/docs/ExportingToExternalFileFormats.md
@@ -27,6 +27,7 @@ If you want a different output format, please use the "--help" flag or use one o
- `-v` enables per-vertex colors (only compatible with .fbx and .ply formats).
- `-r {INT}` controls the resolution of the baked texture maps. For instance, `-r 1024` will export 1024 x 1024 texture maps.
- `--individual` will export each object in a scene in its own individual file.
+- `--omniverse` will prepare the scene for import to IsaacSim or other NVIDIA Omniverse programs. See more in [Exporting to Simulators](./ExportingToSimulators.md).
## :warning: Exporting full Infinigen scenes is only supported for USDC files.
@@ -54,21 +55,15 @@ If you require OBJ/FBX/PLY files for your research, you have a few options:
## Other Known Issues and Limitations
-* Some material features used in Infinigen are not yet supported by this exporter. Specifically, this script only handles Albedo, Roughness and Metallicity maps. Any other procedural parameters of the material will be ignored. Many file formats also have limited support for spatially varying transmission, clearcoat, and sheen. Generally, you should not expect any materials (but especially skin, translucent leaves, glowing lava) to be perfectly reproduced outside of Blender.
-
+* Some material features used in Infinigen are not yet supported by this exporter. Specifically, this script only handles Albedo, Roughness, Normal, and Metallicity maps. Any other procedural parameters of the material will be ignored. Many file formats also have limited support for spatially varying transmission, clearcoat, and sheen. Generally, you should not expect any materials (but especially skin, translucent leaves, glowing lava) to be perfectly reproduced outside of Blender.
* Exporting *animated* 3D files is generally untested and not officially supported. This includes exporting particles, articulated creatures, deforming plants, etc. These features are *in principle* supported by OpenUSD, but are untested by us and not officially supported by this export script.
-
* Assets with transparent materials (water, glass-like materials, etc.) may have incorrect textures for all material parameters after export.
-
* Large scenes and assets may take a long time to export and will crash Blender if you do not have enough RAM. The export results may also be unusably large.
-* When exporting in .fbx format, the embedded roughness texture maps in the file may sometimes be too bright or too dark. The .png roughness map in the folder is accurate, however.
-
-
* .fbx exports occasionally fail due to invalid UV values on complicated geometry. Adjusting the 'island_margin' value in bpy.ops.uv.smart_project() sometimes remedies this
diff --git a/docs/ExportingToSimulators.md b/docs/ExportingToSimulators.md
new file mode 100644
index 000000000..fe8fdadf0
--- /dev/null
+++ b/docs/ExportingToSimulators.md
@@ -0,0 +1,33 @@
+## Exporting an Indoors Scene for Robotics Simulation in NVIDIA IsaacSim
+
+This documentation details how to run a robotics simulation in an exported Infinigen scene using NVIDIA IsaacSim. For more information on scene generation or export, refer to [Hello Room](HelloRoom.md) or [Exporting to External File Formats](./ExportingToExternalFileFormats.md).
+
+:warning: Exported scenes can be imported to any simulator that supports .usd files. However, we have only extensively tested simulator import on **Indoor** scenes using **IsaacSim**, so quality is not guaranteed for Infinigen Nature scenes and/or other simulators.
+
+First, create and export a scene with the commands below:
+
+```bash
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g overhead_singleroom.gin -p compose_indoors.terrain_enabled=False compose_indoors.solve_max_rooms=1
+```
+
+```bash
+python -m infinigen.tools.export --input_folder outputs/indoors/coarse --output_folder outputs/my_export -f usdc -r 1024 --omniverse
+```
+
+Download IsaacSim from [NVIDIA Omniverse](https://developer.nvidia.com/isaac/sim) and set up an IsaacSim conda environment by running the following commands in your IsaacSim Directory (typically ` ~/.local/share/ov/pkg/isaac_sim-2023.1.1`)
+
+```bash
+conda env create -f environment.yml
+conda activate isaac-sim
+source setup_conda_env.sh
+```
+
+Import scene and run a simulation
+
+```bash
+python {PATH_TO/isaac_sim.py} --scene-path outputs/my_export/export_scene.blend/export_scene.usdc --json-path outputs/my_export/export_scene.blend/solve_state.json
+```
+
+:warning: Physical properties are applied based on object relations specified in `solve_state.json`. Scenes can be imported without a `solve_state.json`, but all objects will be static colliders.
+
+
diff --git a/docs/HelloRoom.md b/docs/HelloRoom.md
new file mode 100644
index 000000000..bbace7ba0
--- /dev/null
+++ b/docs/HelloRoom.md
@@ -0,0 +1,126 @@
+# Hello Room: Generate your first Infinigen-Indoors scene
+
+
+
+
+
+
+
+
+## Generate a scene step-by-step
+
+Infinigen has distinct scene generation & rendering stages. We typically run these automatically for you (skip to [Generate scenes automatically](#generating-scenes-automatically)
+
+#### Generate a blender file
+
+First, run ONE command of your choosing from the block below. This will generate a 3D blender file for use in the subsequent steps.
+
+NOTE: `fast_solve.gin` runs the system for fewer solving steps, which sacrifices quality for speed. Remove t
+his to get a more complex & realistic arrangement. You can also remove `compose_indoors.terrain_enabled=False` to add a realistic terrain background (provided you [installed terrain](./Installation.md))
+
+```bash
+# Diningroom, single room only, first person view (~8min CPU runtime)
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin singleroom.gin -p compose_indoors.terrain_enabled=False restrict_solving.restrict_parent_rooms=\[\"DiningRoom\"\]
+
+# Bathroom, single room only, first person view (~13min CPU runtime)
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin singleroom.gin -p compose_indoors.terrain_enabled=False restrict_solving.restrict_parent_rooms=\[\"Bathroom\"\]
+
+# Bedroom, single room only, first person view (~10min CPU runtime)
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin singleroom.gin -p compose_indoors.terrain_enabled=False restrict_solving.restrict_parent_rooms=\[\"Bedroom\"\]
+
+# Kitchen, single room only, first person view (~10min runtime, CPU only)
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin singleroom.gin -p compose_indoors.terrain_enabled=False restrict_solving.restrict_parent_rooms=\[\"Kitchen\"\]
+
+# LivingRoom, single room only, first person view (~11min runtime, CPU only)
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin singleroom.gin -p compose_indoors.terrain_enabled=False restrict_solving.restrict_parent_rooms=\[\"LivingRoom\"\]
+
+# Floor layout, overhead view, no objects (~34 second runtime, CPU only):
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g no_objects.gin overhead.gin -p compose_indoors.terrain_enabled=False
+
+# Single random room with objects, overhead view (~11min. runtime CPU only):
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin overhead.gin singleroom.gin -p compose_indoors.terrain_enabled=False compose_indoors.overhead_cam_enabled=True compose_indoors.solve_max_rooms=1 compose_indoors.invisible_room_ceilings_enabled=True compose_indoors.restrict_single_supported_roomtype=True
+
+# Whole apartment with objects, overhead view:
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve.gin overhead.gin studio.gin -p compose_indoors.terrain_enabled=False
+```
+
+Once complete, you can inspect / fly around `outputs/indoors/coarse/scene.blend` in the blender UI:
+
+```bash
+python -m infinigen.launch_blender outputs/indoors/coarse/scene.blend
+```
+
+You may be prompted to revisit our [Installation.md](./Installation.md#installing-infinigen-as-a-blender-python-script) if blender is not yet installed.
+
+#### Render image and ground truth
+
+Next, run the commands below to render an RGB image and ground truth:
+
+```bash
+# Render RGB images
+python -m infinigen_examples.generate_nature --seed 0 --task render --input_folder outputs/indoors/coarse --output_folder outputs/indoors/frames
+
+# Use blender to extract ground-truth (optional)
+python -m infinigen_examples.generate_nature --seed 0 --task render --input_folder outputs/indoors/coarse --output_folder outputs/indoors/frames -p render.render_image_func=@flat/render_image
+```
+
+Once complete, you can open `outputs/indoors/frames` and navigate to view the results.
+
+#### Next Steps
+
+See [ExportingToExternalFileFormats](./ExportingToExternalFileFormats.md) and [ExportingToSimulators](./ExportingToSimulators.md) to export to OBJ/USD.
+
+We also provide an OpenGL-based ground truth extractor which offers additional ground truth channels, read more about using our ground truth [here](GroundTruthAnnotations.md).
+
+## Generating scenes automatically
+
+To generate a single scene in one command, you can run the following:
+```bash
+screen python -m infinigen.datagen.manage_jobs --output_folder outputs/my_dataset --num_scenes 1000 --pipeline_configs local_256.gin monocular.gin blender_gt.gin indoor_background_configs.gin --configs singleroom.gin --pipeline_overrides get_cmd.driver_script='infinigen_examples.generate_indoors' manage_datagen_jobs.num_concurrent=16 --overrides compose_indoors.restrict_single_supported_roomtype=True
+```
+
+To create a large dataset of many random rooms, we recommend:
+```bash
+screen python -m infinigen.datagen.manage_jobs --output_folder outputs/my_dataset --num_scenes 1000 --pipeline_configs local_256.gin monocular.gin blender_gt.gin indoor_background_configs.gin --configs singleroom.gin --pipeline_overrides get_cmd.driver_script='infinigen_examples.generate_indoors' manage_datagen_jobs.num_concurrent=16 --overrides compose_indoors.restrict_single_supported_roomtype=True
+```
+
+You can inspect `outputs/my_dataset/SEED/` to see the running logs of the subprocesses and output results.
+
+See [ConfiguringInfinigen.md](./ConfiguringInfinigen.md) for documentation on `manage_jobs` and commandline options.
+
+## Developer Guide
+
+More documentation coming soon.
+
+### Restricting the solver to certain rooms / objects
+
+Configuring `compose_indoors()` and ``restrict_solving()` via gin allows you to only solve sub-parts of the default constraint problem:
+
+```
+python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/indoors/coarse -g fast_solve -p \
+ compose_indoors.terrain_enabled=False compose_indoors.solve_medium_enabled=False \
+ restrict_solving.restrict_parent_rooms=[\"Kitchen\"] \
+ restrict_solving.restrict_child_primary=[\"KitchenCounter\"] \
+ restrict_solving.restrict_child_secondary=[\"Sink\"] \
+ restrict_solving.solve_max_rooms=1 \
+ restrict_solving.consgraph_filters=[\"counter\",\"sink\"] \
+ compose_indoors.solve_steps_large=30 compose_indoors.solve_steps_small=30
+```
+
+Each of these commandline args demonstrates a different way in which you can restrict what work the system does:
+- `compose_indoors.terrain_enabled=False compose_indoors.solve_medium_enabled=False` turns off medium objects (paintings/chairs/ceilinglights) and also terrain. You can use this same pattern to disable any `p.run_stage(name, func, ...)` statement found in generate_indoors by specifying f`compose_indoors.{name}_enabled=False`
+- `restrict_solving.solve_max_rooms=1` specifies to only solve objects in 1 room of the house
+- `restrict_solving.restrict_parent_rooms=[\"Kitchen\"]` specifies to only solve objects in kitchen rooms. You can see `infinigen/core/tags.py` for available options.
+- `restrict_solving.restrict_child_primary=[\"KitchenCounter\"]` specifies that when placing objects directly onto the room, we will only consider placing *KitchenCounter* objects, not other types of objects. You can see `infinigen/core/tags.py` for available options.
+- `restrict_solving.restrict_child_secondary=[\"Sink\"]` specifies that when placing objects onto other objects, we will only consider placing *Sink* objects, not other types of objects. You can see `infinigen/core/tags.py` for available options.
+- `restrict_solving.consgraph_filters=[\"counter\",\"sink\"]` says to throw out any `constraints` or `score_terms` keys from `home_constraints()` that do not contain `counter` or `sink` as substrings, producing a simpler constraint graph.
+- `compose_indoors.solve_steps_large=30 compose_indoors.solve_steps_small=30` says to spend fewer optimization steps on large/small objects. You can also do the same for medium. These values override the defaults provided in `fast_solve.gin` and `infinigen_examples/configs_indoor/base.gin`
+
+These settings are intended for debugging or for generating tailored datasets. If you want more granular control over what assets are used for what purposes, please customize `infinigen_examples/indoor_asset_semantics.py` which defines this mapping.
+
+If you are using the commands from [Creating large datasets](#creating-large-datasets) you will instead add these configs as `--overrides` to the end of your command, rather than `-p`
+
+## Run unit tests
+```
+pytest tests/ --disable-warnings
+```
\ No newline at end of file
diff --git a/docs/Installation.md b/docs/Installation.md
index dae6a8a57..d5152facd 100644
--- a/docs/Installation.md
+++ b/docs/Installation.md
@@ -39,13 +39,13 @@ Please install anaconda or miniconda. Platform-specific instructions can be foun
Then, install the following dependencies using the method of your choice. Examples are shown for Ubuntu, Mac ARM and Mac x86.
```bash
# on Ubuntu / Debian / WSL / etc
-sudo apt-get install wget cmake g++ libgles2-mesa-dev libglew-dev libglfw3-dev libglm-dev
+sudo apt-get install wget cmake g++ libgles2-mesa-dev libglew-dev libglfw3-dev libglm-dev zlib1g-dev
# on an Mac ARM (M1/M2/...)
-arch -arm64 brew install wget cmake llvm open-mpi libomp glm glew
+arch -arm64 brew install wget cmake llvm open-mpi libomp glm glew zlib
# on Mac x86_64 (Intel)
-brew install wget cmake llvm open-mpi libomp glm glew
+brew install wget cmake llvm open-mpi libomp glm glew zlib
# on Conda. Useful when you don't have sudo permissions
conda install conda-forge::gxx=11.4.0 mesalib glew glm menpo::glfw3
@@ -57,9 +57,7 @@ export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH
### Installation
-First, download infinigen and set up your environment.
-
-On Linux / Mac / WSL:
+First, download the repo and set up a conda environment (you may need to [install conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html))
```bash
git clone https://github.com/princeton-vl/infinigen.git
cd infinigen
@@ -69,18 +67,15 @@ conda activate infinigen
Then, install the infinigen package using one of the options below:
-:warning: Mac-ARM (M1/M2) users should prefix their installation command with `arch -arm64`
-
```bash
-# Default install (includes CPU Terrain, and CUDA Terrain if available)
-pip install -e .
-
-# Minimal install (objects/materials only, no terrain or optional features)
+# Minimal install (No terrain or opengl GT, ok for Infinigen-Indoors or single-object generation)
INFINIGEN_MINIMAL_INSTALL=True pip install -e .
-# Enable OpenGL GT
-INFINIGEN_INSTALL_CUSTOMGT=True pip install -e .
+# Full install (Terrain & OpenGL-GT enabled, needed for Infinigen-Nature HelloWorld)
+pip install -e .
+# Developer install (includes pytest, ruff, other recommended dev tools)
+pip install -e ".[dev]"
```
:exclamation: If you encounter any issues with the above, please add `-vv > logs.txt 2>&1` to the end of your command and run again, then provide the resulting logs.txt file as an attachment when making a Github Issue.
diff --git a/docs/images/hello_room/dining.png b/docs/images/hello_room/dining.png
new file mode 100644
index 000000000..b0478f7cd
Binary files /dev/null and b/docs/images/hello_room/dining.png differ
diff --git a/docs/images/hello_room/dining_blender.png b/docs/images/hello_room/dining_blender.png
new file mode 100644
index 000000000..846d6e054
Binary files /dev/null and b/docs/images/hello_room/dining_blender.png differ
diff --git a/docs/images/hello_room/dining_depth.png b/docs/images/hello_room/dining_depth.png
new file mode 100644
index 000000000..f43784fc3
Binary files /dev/null and b/docs/images/hello_room/dining_depth.png differ
diff --git a/docs/images/hello_room/dining_obj.png b/docs/images/hello_room/dining_obj.png
new file mode 100644
index 000000000..cc3f1d060
Binary files /dev/null and b/docs/images/hello_room/dining_obj.png differ
diff --git a/docs/images/infinigen.png b/docs/images/infinigen.png
new file mode 100644
index 000000000..3f0dcd938
Binary files /dev/null and b/docs/images/infinigen.png differ
diff --git a/infinigen/OcMesher b/infinigen/OcMesher
index 2cdcbacbe..4e5fad7b0 160000
--- a/infinigen/OcMesher
+++ b/infinigen/OcMesher
@@ -1 +1 @@
-Subproject commit 2cdcbacbe62ef79dc6031e0131f916266b7372e3
+Subproject commit 4e5fad7b0dd495444acf3ab2037bf08dd4b5d676
diff --git a/infinigen/__init__.py b/infinigen/__init__.py
index ab5061d11..da6aaa4ca 100644
--- a/infinigen/__init__.py
+++ b/infinigen/__init__.py
@@ -1,3 +1,3 @@
import logging
-__version__ = "1.3.4"
+__version__ = "1.4.0"
diff --git a/infinigen/assets/appliances/__init__.py b/infinigen/assets/appliances/__init__.py
new file mode 100644
index 000000000..795254199
--- /dev/null
+++ b/infinigen/assets/appliances/__init__.py
@@ -0,0 +1,5 @@
+from .oven import OvenFactory
+from .beverage_fridge import BeverageFridgeFactory
+from .dishwasher import DishwasherFactory
+from .microwave import MicrowaveFactory
+from .tv import TVFactory, MonitorFactory
diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py
new file mode 100644
index 000000000..9ab8b5278
--- /dev/null
+++ b/infinigen/assets/appliances/beverage_fridge.py
@@ -0,0 +1,735 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core.util.blender import delete
+from infinigen.core.util.bevelling import get_bevel_edges, add_bevel, complete_bevel, complete_no_bevel
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.material_assignments import AssetList
+
+class BeverageFridgeFactory(AssetFactory):
+
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]):
+ super(BeverageFridgeFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['BeverageFridgeFactory']()
+ params = {
+ "Surface": material_assignments['surface'].assign_material(),
+ "Front": material_assignments['front'].assign_material(),
+ "Handle": material_assignments['handle'].assign_material(),
+ "Back": material_assignments['back'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ depth = 1 + N(0, 0.1)
+ width = 1 + N(0, 0.1)
+ height = 1 + N(0, 0.1)
+ # depth, width, height = dimensions
+ door_thickness = U(0.05, 0.1) * depth
+ door_rotation = 0 # Set to 0 for now
+
+ rack_radius = U(0.01, 0.02) * depth
+ rack_h_amount = RI(2, 4)
+ rack_d_amount = RI(4, 6)
+ brand_name = "BrandName"
+
+ params = {
+ "Depth": depth,
+ "Width": width,
+ "Height": height,
+ "DoorThickness": door_thickness,
+ "DoorRotation": door_rotation,
+ "RackRadius": rack_radius,
+ "RackHAmount": rack_h_amount,
+ "RackDAmount": rack_d_amount,
+ "BrandName": brand_name,
+ }
+ return params
+
+ def create_asset(self, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_beverage_fridge_geometry(preprocess=True), ng_inputs=self.params, apply=True)
+ bevel_edges = get_bevel_edges(obj)
+ delete(obj)
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_beverage_fridge_geometry(), ng_inputs=self.params, apply=True)
+ obj = add_bevel(obj, bevel_edges, offset=0.01)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+@node_utils.to_nodegroup('nodegroup_oven_rack', singleton=False, type='GeometryNodeTree')
+def nodegroup_oven_rack(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Height', 2.0000),
+ ('NodeSocketFloatDistance', 'Radius', 0.0200),
+ ('NodeSocketInt', 'Amount', 5)])
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': group_input.outputs["Width"], 'Height': group_input.outputs["Height"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_3, 'End': combine_xyz_4})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': curve_line})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': group_input.outputs["Amount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply_2, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz})
+
+ duplicate_elements_1 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': group_input.outputs["Amount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ divide_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply_4, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: divide_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements_1.outputs["Geometry"], 'Offset': combine_xyz_1})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [quadrilateral, set_position, set_position_1]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Radius"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': join_geometry, 'Profile Curve': curve_circle.outputs["Curve"], 'Fill Caps': True})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': curve_to_mesh}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_text', singleton=False, type='GeometryNodeTree')
+def nodegroup_text(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Translation', (1.5000, 0.0000, 0.0000)),
+ ('NodeSocketString', 'String', 'BrandName'),
+ ('NodeSocketFloatDistance', 'Size', 0.0500),
+ ('NodeSocketFloat', 'Offset Scale', 0.0020)])
+
+ string_to_curves = nw.new_node('GeometryNodeStringToCurves',
+ input_kwargs={'String': group_input.outputs["String"], 'Size': group_input.outputs["Size"]},
+ attrs={'align_y': 'BOTTOM_BASELINE', 'align_x': 'CENTER'})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': string_to_curves.outputs["Curve Instances"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh,
+ input_kwargs={'Mesh': fill_curve, 'Offset Scale': group_input.outputs["Offset Scale"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': extrude_mesh.outputs["Mesh"], 'Translation': group_input.outputs["Translation"], 'Rotation': (1.5708, 0.0000, 1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'length', 0.0000),
+ ('NodeSocketFloat', 'thickness', 0.0200)])
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map', 3: cube_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["length"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [store_named_attribute, transform]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz_3})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"], 1: group_input.outputs["width"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': add, 'Z': group_input.outputs["thickness"]})
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"]}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: multiply_2})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': add_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_2})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_center', singleton=False, type='GeometryNodeTree')
+def nodegroup_center(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketVector', 'Vector', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketFloat', 'MarginX', 0.5000),
+ ('NodeSocketFloat', 'MarginY', 0.0000),
+ ('NodeSocketFloat', 'MarginZ', 0.0000)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Vector"], 1: bounding_box.outputs["Min"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract.outputs["Vector"]})
+
+ greater_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bounding_box.outputs["Max"], 1: group_input.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract_1.outputs["Vector"]})
+
+ greater_than_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than, 1: greater_than_1})
+
+ greater_than_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN'})
+
+ greater_than_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_1 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_2, 1: greater_than_3})
+
+ op_and_2 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and, 1: op_and_1})
+
+ greater_than_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ greater_than_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_3 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_4, 1: greater_than_5})
+
+ op_and_4 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_2, 1: op_and_3})
+
+ op_not = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_4}, attrs={'operation': 'NOT'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'In': op_and_4, 'Out': op_not}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2)])
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': group_input.outputs["Size"], 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Name': 'uv_map'},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_add = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Size"], 1: (0.5000, 0.5000, 0.5000), 2: group_input.outputs["Pos"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': multiply_add.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_hollow_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_hollow_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2),
+ ('NodeSocketFloat', 'Thickness', 0.0000),
+ ('NodeSocketBool', 'Switch1', False),
+ ('NodeSocketBool', 'Switch2', False),
+ ('NodeSocketBool', 'Switch3', False),
+ ('NodeSocketBool', 'Switch4', False),
+ ('NodeSocketBool', 'Switch5', False),
+ ('NodeSocketBool', 'Switch6', False)])
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Size"]})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Thickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Thickness"], 'Y': subtract, 'Z': subtract_1})
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"]}, attrs={'operation': 'MULTIPLY'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Pos"]})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["X"]})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Size"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale.outputs["Vector"]})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': subtract_2})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_5})
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch3"], 14: transform_2})
+
+ subtract_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': subtract_3, 'Z': group_input.outputs["Thickness"]})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_2, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute_4 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map', 3: cube_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': add_3, 'Z': subtract_4})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_4, 'Translation': combine_xyz_3})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch2"], 14: transform_1})
+
+ subtract_5 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': subtract_5, 'Z': group_input.outputs["Thickness"]})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': add_5, 'Z': add_6})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1})
+
+ switch = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch1"], 14: transform})
+
+ subtract_6 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_7 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Thickness"], 'Y': subtract_6, 'Z': subtract_7})
+
+ cube_3 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_6, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute_5 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_3.outputs["Mesh"], 'Name': 'uv_map', 3: cube_3.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subtract_8 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["X"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_9 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_8, 'Y': add_7, 'Z': subtract_9})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_5, 'Translation': combine_xyz_7})
+
+ switch_3 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch4"], 14: transform_3})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': group_input.outputs["Thickness"], 'Z': separate_xyz.outputs["Z"]})
+
+ cube_4 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_9, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_4.outputs["Mesh"], 'Name': 'uv_map', 3: cube_4.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_8 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["X"], 1: separate_xyz_2.outputs["X"]})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["Y"], 1: multiply_1})
+
+ add_10 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["Z"], 1: separate_xyz_2.outputs["Z"]})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_8, 'Y': add_9, 'Z': add_10})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_8})
+
+ switch_4 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch5"], 14: transform_4})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': group_input.outputs["Thickness"], 'Z': separate_xyz.outputs["Z"]})
+
+ cube_5 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_10, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2})
+
+ store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_5.outputs["Mesh"], 'Name': 'uv_map', 3: cube_5.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_11 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ subtract_10 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ add_12 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_11, 'Y': subtract_10, 'Z': add_12})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_3, 'Translation': combine_xyz_11})
+
+ switch_5 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch6"], 14: transform_5})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [switch_2.outputs[6], switch_1.outputs[6], switch.outputs[6], switch_3.outputs[6], switch_4.outputs[6], switch_5.outputs[6]]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_beverage_fridge_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_beverage_fridge_geometry(nw: NodeWrangler, preprocess: bool = False):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Depth', 1.0000),
+ ('NodeSocketFloat', 'Width', 1.0000),
+ ('NodeSocketFloat', 'Height', 1.0000),
+ ('NodeSocketFloat', 'DoorThickness', 0.0700),
+ ('NodeSocketFloat', 'DoorRotation', 0.0000),
+ ('NodeSocketFloatDistance', 'RackRadius', 0.0100),
+ ('NodeSocketInt', 'RackDAmount', 5),
+ ('NodeSocketInt', 'RackHAmount', 2),
+ ('NodeSocketString', 'BrandName', 'BrandName'),
+ ('NodeSocketMaterial', 'Surface', None),
+ ('NodeSocketMaterial', 'Front', None),
+ ('NodeSocketMaterial', 'Handle', None),
+ ('NodeSocketMaterial', 'Back', None)])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Depth"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ hollowcube = nw.new_node(nodegroup_hollow_cube().name,
+ input_kwargs={'Size': combine_xyz, 'Thickness': group_input.outputs["DoorThickness"], 'Switch2': True, 'Switch4': True})
+
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': hollowcube, 'Material': group_input.outputs["Surface"]})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': set_material_1, 'Level': 0})
+
+ # set_shade_smooth_2 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': subdivide_mesh})
+
+ body = nw.new_node(Nodes.Reroute, input_kwargs={'Input': subdivide_mesh}, label='Body')
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["DoorThickness"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ cube = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_1, 'Pos': combine_xyz_2})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ center = nw.new_node(nodegroup_center().name,
+ input_kwargs={'Geometry': cube, 'Vector': position, 'MarginX': -1.0000, 'MarginY': 0.1000, 'MarginZ': 0.1500})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube, 'Selection': center.outputs["In"], 'Material': group_input.outputs["Front"]})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_2, 'Selection': center.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+ # set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_3})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.8000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ handle = nw.new_node(nodegroup_handle().name,
+ input_kwargs={'width': multiply, 'length': multiply_1, 'thickness': multiply_2})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.9000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_13 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': multiply_3, 'Z': multiply_4})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': handle, 'Translation': combine_xyz_13, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ set_material_8 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_1, 'Material': group_input.outputs["Handle"]})
+
+ geometry_to_instance_4 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': set_material_8})
+
+ rotate_instances_2 = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': geometry_to_instance_4, 'Rotation': (-1.5708, 0.0000, 0.0000), 'Pivot Point': combine_xyz_13})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': multiply_5, 'Z': 0.0300})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ text = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_12, 'String': group_input.outputs["BrandName"], 'Size': multiply_6, 'Offset Scale': 0.0020})
+
+ text = complete_no_bevel(nw, text, preprocess)
+
+ set_material_9 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': text, 'Material': group_input.outputs["Handle"]})
+
+ rotate_instances_2 = complete_bevel(nw, rotate_instances_2, preprocess)
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_3, rotate_instances_2, set_material_9]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3})
+
+ z = nw.scalar_multiply(group_input.outputs["DoorRotation"], 1 if not preprocess else 0)
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': z})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"], 'Y': group_input.outputs["Width"]})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': geometry_to_instance, 'Rotation': combine_xyz_3, 'Pivot Point': combine_xyz_4})
+
+ door = nw.new_node(Nodes.Reroute, input_kwargs={'Input': nw.new_node(Nodes.RealizeInstances, [rotate_instances])}, label='door')
+
+ multiply_7 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: multiply_7},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_8 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: multiply_8},
+ attrs={'operation': 'SUBTRACT'})
+
+ ovenrack = nw.new_node(nodegroup_oven_rack().name,
+ input_kwargs={'Width': subtract, 'Height': subtract_1, 'Radius': group_input.outputs["RackRadius"], 'Amount': group_input.outputs["RackDAmount"]})
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': ovenrack})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance_1, 'Amount': group_input.outputs["RackHAmount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_9 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: 1.0000})
+
+ multiply_11 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Height"], 1: multiply_11},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["RackHAmount"], 1: 1.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: add_3}, attrs={'operation': 'DIVIDE'})
+
+ multiply_12 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_9, 'Y': multiply_10, 'Z': multiply_12})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz_5})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_position, 'Material': group_input.outputs["Handle"]})
+
+ racks = nw.new_node(Nodes.Reroute, input_kwargs={'Input': nw.new_node(Nodes.RealizeInstances, [set_material])}, label='racks')
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ reroute_10 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': add_4})
+
+ reroute_11 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Width"]})
+
+ reroute_8 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["DoorThickness"]})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_10, 'Y': reroute_11, 'Z': reroute_8})
+
+ reroute_9 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Height"]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': reroute_9})
+
+ cube_1 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_6, 'Pos': combine_xyz_7})
+
+ set_material_5 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube_1, 'Material': group_input.outputs["Back"]})
+
+ # set_shade_smooth_1 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_5})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': set_material_5})
+
+ heater = nw.new_node(Nodes.Reroute, input_kwargs={'Input': join_geometry_2}, label='heater')
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [body, door, racks, heater]})
+
+ geometry = nw.new_node(Nodes.RealizeInstances,[join_geometry])
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': geometry})
diff --git a/infinigen/assets/appliances/dishwasher.py b/infinigen/assets/appliances/dishwasher.py
new file mode 100644
index 000000000..0b1694a3b
--- /dev/null
+++ b/infinigen/assets/appliances/dishwasher.py
@@ -0,0 +1,929 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+
+from infinigen.assets.materials import metal
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core.util.blender import delete
+from infinigen.core.util.bevelling import get_bevel_edges, add_bevel, complete_bevel, complete_no_bevel
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.material_assignments import AssetList
+
+class DishwasherFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]):
+ super(DishwasherFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['DishwasherFactory']()
+ params = {
+ "Surface": material_assignments['surface'].assign_material(),
+ "Front": material_assignments['front'].assign_material(),
+ "WhiteMetal": material_assignments['white_metal'].assign_material(),
+ "Top": material_assignments['top'].assign_material(),
+ 'NameMaterial': material_assignments['name_material'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # depth, width, height = dimensions
+ depth = 1 + N(0, 0.1)
+ width = 1 + N(0, 0.1)
+ height = 1 + N(0, 0.1)
+ door_thickness = U(0.05, 0.1) * depth
+ door_rotation = 0 # Set to 0 for now
+
+ rack_radius = U(0.01, 0.02) * depth
+ rack_h_amount = RI(2, 3)
+ brand_name = "BrandName"
+
+ params = {
+ "Depth": depth,
+ "Width": width,
+ "Height": height,
+ "DoorThickness": door_thickness,
+ "DoorRotation": door_rotation,
+ "RackRadius": rack_radius,
+ "RackAmount": rack_h_amount,
+ "BrandName": brand_name,
+ }
+ return params
+
+ def create_asset(self, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_dishwasher_geometry(preprocess=True), ng_inputs=self.params, apply=True)
+ bevel_edges = get_bevel_edges(obj)
+ delete(obj)
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_dishwasher_geometry(), ng_inputs=self.params, apply=True)
+ obj = add_bevel(obj, bevel_edges, offset=0.01)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+@node_utils.to_nodegroup('nodegroup_dish_rack', singleton=False, type='GeometryNodeTree')
+def nodegroup_dish_rack(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral')
+
+ curve_line = nw.new_node(Nodes.CurveLine,
+ input_kwargs={'Start': (0.0000, -1.0000, 0.0000), 'End': (0.0000, 1.0000, 0.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Depth', 2.0000),
+ ('NodeSocketFloatDistance', 'Width', 2.0000), ('NodeSocketFloatDistance', 'Radius', 0.0200),
+ ('NodeSocketInt', 'Amount', 5), ('NodeSocketFloat', 'Height', 0.5000)])
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'Y': -1.0000, 'Z': group_input.outputs["Height"]})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine,
+ input_kwargs={'Start': (0.0000, -1.0000, 0.0000), 'End': combine_xyz_4})
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance',
+ input_kwargs={'Geometry': curve_line_1})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Amount"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ duplicate_elements_2 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance_1, 'Amount': multiply},
+ attrs={'domain': 'INSTANCE'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: 1.0000, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_2.outputs["Duplicate Index"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements_2.outputs["Geometry"],
+ 'Offset': combine_xyz_3
+ })
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_line, set_position_2]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance',
+ input_kwargs={'Geometry': join_geometry_1})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': multiply},
+ attrs={'domain': 'INSTANCE'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={
+ 0: duplicate_elements.outputs["Duplicate Index"],
+ 1: group_input.outputs["Amount"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements.outputs["Geometry"],
+ 'Offset': combine_xyz
+ })
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': set_position, 'Rotation': (0.0000, 0.0000, 1.5708)})
+
+ duplicate_elements_1 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': multiply},
+ attrs={'domain': 'INSTANCE'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: duplicate_elements_1.outputs["Duplicate Index"],
+ 1: group_input.outputs["Amount"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements_1.outputs["Geometry"],
+ 'Offset': combine_xyz_1
+ })
+
+ quadrilateral_1 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral')
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.8000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_4})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': quadrilateral_1, 'Translation': combine_xyz_5})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [quadrilateral, transform_1, set_position_1, transform_2]
+ })
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Radius"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': join_geometry,
+ 'Profile Curve': curve_circle.outputs["Curve"],
+ 'Fill Caps': True
+ })
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Y': multiply_6, 'Z': 0.5000})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': curve_to_mesh,
+ 'Rotation': (0.0000, 0.0000, 1.5708),
+ 'Scale': combine_xyz_2
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': transform},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_text', singleton=False, type='GeometryNodeTree')
+def nodegroup_text(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[
+ ('NodeSocketVectorTranslation', 'Translation', (1.5000, 0.0000, 0.0000)),
+ ('NodeSocketString', 'String', 'BrandName'), ('NodeSocketFloatDistance', 'Size', 0.0500),
+ ('NodeSocketFloat', 'Offset Scale', 0.0020)])
+
+ string_to_curves = nw.new_node('GeometryNodeStringToCurves', input_kwargs={
+ 'String': group_input.outputs["String"],
+ 'Size': group_input.outputs["Size"]
+ }, attrs={'align_y': 'BOTTOM_BASELINE', 'align_x': 'CENTER'})
+
+ fill_curve = nw.new_node(Nodes.FillCurve,
+ input_kwargs={'Curve': string_to_curves.outputs["Curve Instances"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={
+ 'Mesh': fill_curve,
+ 'Offset Scale': group_input.outputs["Offset Scale"]
+ })
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': extrude_mesh.outputs["Mesh"],
+ 'Translation': group_input.outputs["Translation"],
+ 'Rotation': (1.5708, 0.0000, 1.5708)
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'length', 0.0000), ('NodeSocketFloat', 'thickness', 0.0200)])
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_1.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_1.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["length"]})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [store_named_attribute, transform]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz_3})
+
+ add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["length"], 1: group_input.outputs["width"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["width"],
+ 'Y': add,
+ 'Z': group_input.outputs["thickness"]
+ })
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_2.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_2.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: multiply_2})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': add_1})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_2})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+
+@node_utils.to_nodegroup('nodegroup_center', singleton=False, type='GeometryNodeTree')
+def nodegroup_center(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketVector', 'Vector', (0.0000, 0.0000, 0.0000)), ('NodeSocketFloat', 'MarginX', 0.5000),
+ ('NodeSocketFloat', 'MarginY', 0.0000), ('NodeSocketFloat', 'MarginZ', 0.0000)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Vector"], 1: bounding_box.outputs["Min"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract.outputs["Vector"]})
+
+ greater_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bounding_box.outputs["Max"], 1: group_input.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract_1.outputs["Vector"]})
+
+ greater_than_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: separate_xyz_1.outputs["X"],
+ 1: group_input.outputs["MarginX"]
+ }, attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than, 1: greater_than_1})
+
+ greater_than_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN'})
+
+ greater_than_3 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: separate_xyz_1.outputs["Y"],
+ 1: group_input.outputs["MarginY"]
+ }, attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_1 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_2, 1: greater_than_3})
+
+ op_and_2 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and, 1: op_and_1})
+
+ greater_than_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ greater_than_5 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: separate_xyz_1.outputs["Z"],
+ 1: group_input.outputs["MarginZ"]
+ }, attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_3 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_4, 1: greater_than_5})
+
+ op_and_4 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_2, 1: op_and_3})
+
+ op_not = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_4}, attrs={'operation': 'NOT'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'In': op_and_4, 'Out': op_not},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2)])
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={
+ 'Size': group_input.outputs["Size"],
+ 'Vertices X': group_input.outputs["Resolution"],
+ 'Vertices Y': group_input.outputs["Resolution"],
+ 'Vertices Z': group_input.outputs["Resolution"]
+ })
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Name': 'uv_map'},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_add = nw.new_node(Nodes.VectorMath, input_kwargs={
+ 0: group_input.outputs["Size"],
+ 1: (0.5000, 0.5000, 0.5000),
+ 2: group_input.outputs["Pos"]
+ }, attrs={'operation': 'MULTIPLY_ADD'})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': store_named_attribute,
+ 'Translation': multiply_add.outputs["Vector"]
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+
+@node_utils.to_nodegroup('nodegroup_hollow_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_hollow_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2),
+ ('NodeSocketFloat', 'Thickness', 0.0000),
+ ('NodeSocketBool', 'Switch1', False),
+ ('NodeSocketBool', 'Switch2', False),
+ ('NodeSocketBool', 'Switch3', False),
+ ('NodeSocketBool', 'Switch4', False),
+ ('NodeSocketBool', 'Switch5', False),
+ ('NodeSocketBool', 'Switch6', False)])
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Size"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Thickness"],
+ 'Y': subtract,
+ 'Z': subtract_1
+ })
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_2.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_2.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Pos"]})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["X"]})
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: group_input.outputs["Size"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale.outputs["Vector"]})
+
+ add_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': subtract_2})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_5})
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch3"], 14: transform_2})
+
+ subtract_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': separate_xyz.outputs["X"],
+ 'Y': subtract_3,
+ 'Z': group_input.outputs["Thickness"]
+ })
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_2, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_4 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_1.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_1.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': add_3, 'Z': subtract_4})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_4, 'Translation': combine_xyz_3})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch2"], 14: transform_1})
+
+ subtract_5 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': separate_xyz.outputs["X"],
+ 'Y': subtract_5,
+ 'Z': group_input.outputs["Thickness"]
+ })
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': add_5, 'Z': add_6})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1})
+
+ switch = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch1"], 14: transform})
+
+ subtract_6 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ subtract_7 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Thickness"],
+ 'Y': subtract_6,
+ 'Z': subtract_7
+ })
+
+ cube_3 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_6, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_5 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_3.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_3.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subtract_8 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["X"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_7 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_9 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_8, 'Y': add_7, 'Z': subtract_9})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_5, 'Translation': combine_xyz_7})
+
+ switch_3 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch4"], 14: transform_3})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': separate_xyz.outputs["X"],
+ 'Y': group_input.outputs["Thickness"],
+ 'Z': separate_xyz.outputs["Z"]
+ })
+
+ cube_4 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_9, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_4.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_4.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_8 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: separate_xyz_2.outputs["X"]})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["Y"], 1: multiply_1})
+
+ add_10 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Z"], 1: separate_xyz_2.outputs["Z"]})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_8, 'Y': add_9, 'Z': add_10})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_8})
+
+ switch_4 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch5"], 14: transform_4})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': separate_xyz.outputs["X"],
+ 'Y': group_input.outputs["Thickness"],
+ 'Z': separate_xyz.outputs["Z"]
+ })
+
+ cube_5 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_10, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube_5.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube_5.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_11 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ subtract_10 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_12 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_11, 'Y': subtract_10, 'Z': add_12})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_3, 'Translation': combine_xyz_11})
+
+ switch_5 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch6"], 14: transform_5})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [switch_2.outputs[6], switch_1.outputs[6], switch.outputs[6], switch_3.outputs[6],
+ switch_4.outputs[6], switch_5.outputs[6]]
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+
+@node_utils.to_nodegroup('nodegroup_dishwasher_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_dishwasher_geometry(nw: NodeWrangler, preprocess: bool = False):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Depth', 1.0000),
+ ('NodeSocketFloat', 'Width', 1.0000),
+ ('NodeSocketFloat', 'Height', 1.0000),
+ ('NodeSocketFloat', 'DoorThickness', 0.0700),
+ ('NodeSocketFloat', 'DoorRotation', 0.0000),
+ ('NodeSocketFloatDistance', 'RackRadius', 0.0100),
+ ('NodeSocketInt', 'RackAmount', 2),
+ ('NodeSocketString', 'BrandName', 'BrandName'),
+ ('NodeSocketMaterial', 'Surface', None),
+ ('NodeSocketMaterial', 'Front', None),
+ ('NodeSocketMaterial', 'Top', None),
+ ('NodeSocketMaterial', 'WhiteMetal', None),
+ ('NodeSocketMaterial', 'NameMaterial', None)])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Depth"],
+ 'Y': group_input.outputs["Width"],
+ 'Z': group_input.outputs["Height"]
+ })
+
+ hollowcube = nw.new_node(nodegroup_hollow_cube().name, input_kwargs={
+ 'Size': combine_xyz,
+ 'Thickness': group_input.outputs["DoorThickness"],
+ 'Switch2': True,
+ 'Switch4': True
+ })
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': hollowcube, 'Material': group_input.outputs["Surface"]})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': set_material_1, 'Level': 0})
+
+ # set_shade_smooth_2 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': subdivide_mesh})
+
+ body = nw.new_node(Nodes.Reroute, input_kwargs={'Input': subdivide_mesh}, label='Body')
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["DoorThickness"],
+ 'Y': group_input.outputs["Width"],
+ 'Z': group_input.outputs["Height"]
+ })
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ cube = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_1, 'Pos': combine_xyz_2})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ center = nw.new_node(nodegroup_center().name, input_kwargs={
+ 'Geometry': cube,
+ 'Vector': position,
+ 'MarginX': -1.0000,
+ 'MarginY': 0.1000,
+ 'MarginZ': 0.1500
+ })
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube, 'Selection': center.outputs["In"], 'Material': group_input.outputs["Front"]})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_2, 'Selection': center.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+
+ # set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_3})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.0500},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.8000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ handle = nw.new_node(nodegroup_handle().name,
+ input_kwargs={'width': multiply, 'length': multiply_1, 'thickness': multiply_2})
+
+ add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.9500},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_13 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': multiply_3, 'Z': multiply_4})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': handle,
+ 'Translation': combine_xyz_13,
+ 'Rotation': (0.0000, 1.5708, 0.0000)
+ })
+
+ set_material_8 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_1, 'Material': group_input.outputs["WhiteMetal"]})
+
+ add_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': multiply_5, 'Z': 0.0300})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.0500},
+ attrs={'operation': 'MULTIPLY'})
+
+ text = nw.new_node(nodegroup_text().name, input_kwargs={
+ 'Translation': combine_xyz_12,
+ 'String': group_input.outputs["BrandName"],
+ 'Size': multiply_6
+ })
+
+ text = complete_no_bevel(nw, text, preprocess)
+
+ set_material_9 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': text, 'Material': group_input.outputs["NameMaterial"]})
+
+ set_material_8 = complete_bevel(nw, set_material_8, preprocess)
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_3, set_material_8, set_material_9]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance',
+ input_kwargs={'Geometry': join_geometry_3})
+
+ y = nw.scalar_multiply(group_input.outputs["DoorRotation"], 1 if not preprocess else 0)
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': y})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances, input_kwargs={
+ 'Instances': geometry_to_instance,
+ 'Rotation': combine_xyz_3,
+ 'Pivot Point': combine_xyz_4
+ })
+
+ rotate_instances = nw.new_node(Nodes.RealizeInstances, [rotate_instances])
+
+ door = nw.new_node(Nodes.Reroute, input_kwargs={'Input': rotate_instances}, label='door')
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: multiply_7},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: multiply_8},
+ attrs={'operation': 'SUBTRACT'})
+
+ dishrack = nw.new_node(nodegroup_dish_rack().name, input_kwargs={
+ 'Depth': subtract_1,
+ 'Width': subtract,
+ 'Radius': group_input.outputs["RackRadius"],
+ 'Amount': 4,
+ 'Height': 0.1000
+ })
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': dishrack})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements, input_kwargs={
+ 'Geometry': geometry_to_instance_1,
+ 'Amount': group_input.outputs["RackAmount"]
+ }, attrs={'domain': 'INSTANCE'})
+
+ multiply_9 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: 1.0000})
+
+ multiply_11 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: multiply_11},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["RackAmount"], 1: 1.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: add_3}, attrs={'operation': 'DIVIDE'})
+
+ multiply_12 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_9, 'Y': multiply_10, 'Z': multiply_12})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements.outputs["Geometry"],
+ 'Offset': combine_xyz_5
+ })
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_position, 'Material': group_input.outputs["Surface"]})
+
+ set_material = nw.new_node(Nodes.RealizeInstances, [set_material])
+
+ racks = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material}, label='racks')
+
+ add_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ reroute_10 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': add_4})
+
+ reroute_11 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Width"]})
+
+ reroute_8 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["DoorThickness"]})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': reroute_10, 'Y': reroute_11, 'Z': reroute_8})
+
+ reroute_9 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Height"]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': reroute_9})
+
+ cube_1 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_6, 'Pos': combine_xyz_7})
+
+ set_material_5 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube_1, 'Material': group_input.outputs["Top"]})
+
+ # set_shade_smooth_1 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_5})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': set_material_5})
+
+ heater = nw.new_node(Nodes.Reroute, input_kwargs={'Input': join_geometry_2}, label='heater')
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [body, door, racks, heater]})
+
+ geometry = nw.new_node(Nodes.RealizeInstances, [join_geometry])
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
diff --git a/infinigen/assets/appliances/microwave.py b/infinigen/assets/appliances/microwave.py
new file mode 100644
index 000000000..3e21353b3
--- /dev/null
+++ b/infinigen/assets/appliances/microwave.py
@@ -0,0 +1,447 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+
+from infinigen.assets.utils.misc import generate_text
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import delete
+from infinigen.core.util.bevelling import get_bevel_edges, add_bevel, complete_bevel, complete_no_bevel
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.material_assignments import AssetList
+
+
+class MicrowaveFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]):
+ super(MicrowaveFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['MicrowaveFactory']()
+ params = {
+ "Surface": material_assignments['surface'].assign_material(),
+ "Back": material_assignments['back'].assign_material(),
+ "BlackGlass": material_assignments['black_glass'].assign_material(),
+ "Glass": material_assignments['glass'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ depth = U(0.5, 0.7)
+ width = U(0.6, 1.0)
+ height = U(0.35, 0.45)
+ panel_width = U(0.2, 0.4)
+ margin_z = U(0.05, 0.1)
+ door_thickness = U(0.02, 0.04)
+ door_margin = U(0.03, 0.1)
+ door_rotation = 0 # Set to 0 for now
+ brand_name = generate_text()
+ params = {
+ "Depth": depth,
+ "Width": width,
+ "Height": height,
+ "PanelWidth": panel_width,
+ "MarginZ": margin_z,
+ "DoorThickness": door_thickness,
+ "DoorMargin": door_margin,
+ "DoorRotation": door_rotation,
+ "BrandName": brand_name,
+ }
+ return params
+
+ def create_asset(self, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_microwave_geometry(preprocess=True), ng_inputs=self.params, apply=True)
+ bevel_edges = get_bevel_edges(obj)
+ delete(obj)
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_microwave_geometry(), ng_inputs=self.params, apply=True)
+ obj = add_bevel(obj, bevel_edges)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+@node_utils.to_nodegroup('nodegroup_plate', singleton=False, type='GeometryNodeTree')
+def nodegroup_plate(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 128})
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Start Handle': (0.0000, 0.0000, 0.0000), 'End': (1.0000, 0.0000, 0.4000)})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': bezier_segment, 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': curve_circle.outputs["Curve"], 'Profile Curve': transform})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketVectorXYZ', 'Scale', (1.0000, 1.0000, 1.0000))])
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh, 'Scale': group_input.outputs["Scale"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': transform_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_text', singleton=False, type='GeometryNodeTree')
+def nodegroup_text(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Translation', (1.5000, 0.0000, 0.0000)),
+ ('NodeSocketString', 'String', 'BrandName'),
+ ('NodeSocketFloatDistance', 'Size', 0.0500),
+ ('NodeSocketFloat', 'Offset Scale', 0.0020)])
+
+ string_to_curves = nw.new_node('GeometryNodeStringToCurves',
+ input_kwargs={'String': group_input.outputs["String"], 'Size': group_input.outputs["Size"]},
+ attrs={'align_y': 'BOTTOM_BASELINE', 'align_x': 'CENTER'})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': string_to_curves.outputs["Curve Instances"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh,
+ input_kwargs={'Mesh': fill_curve, 'Offset Scale': group_input.outputs["Offset Scale"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': extrude_mesh.outputs["Mesh"], 'Translation': group_input.outputs["Translation"], 'Rotation': (1.5708, 0.0000, 1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_center', singleton=False, type='GeometryNodeTree')
+def nodegroup_center(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketVector', 'Vector', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketFloat', 'MarginX', 0.5000),
+ ('NodeSocketFloat', 'MarginY', 0.0000),
+ ('NodeSocketFloat', 'MarginZ', 0.0000)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Vector"], 1: bounding_box.outputs["Min"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract.outputs["Vector"]})
+
+ greater_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bounding_box.outputs["Max"], 1: group_input.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract_1.outputs["Vector"]})
+
+ greater_than_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than, 1: greater_than_1})
+
+ greater_than_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN'})
+
+ greater_than_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_1 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_2, 1: greater_than_3})
+
+ op_and_2 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and, 1: op_and_1})
+
+ greater_than_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ greater_than_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_3 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_4, 1: greater_than_5})
+
+ op_and_4 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_2, 1: op_and_3})
+
+ op_not = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_4}, attrs={'operation': 'NOT'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'In': op_and_4, 'Out': op_not}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 10)])
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': group_input.outputs["Size"], 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Name': 'uv_map'},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_add = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Size"], 1: (0.5000, 0.5000, 0.5000), 2: group_input.outputs["Pos"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': multiply_add.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_microwave_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_microwave_geometry(nw: NodeWrangler, preprocess: bool=False):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Depth', 0.0000),
+ ('NodeSocketFloat', 'Width', 0.0000),
+ ('NodeSocketFloat', 'Height', 0.0000),
+ ('NodeSocketFloat', 'PanelWidth', 0.5000),
+ ('NodeSocketFloat', 'MarginZ', 0.0000),
+ ('NodeSocketFloat', 'DoorThickness', 0.0000),
+ ('NodeSocketFloat', 'DoorMargin', 0.0500),
+ ('NodeSocketFloat', 'DoorRotation', 0.0000),
+ ('NodeSocketString', 'BrandName', 'BrandName'),
+ ('NodeSocketMaterial', 'Surface', None),
+ ('NodeSocketMaterial', 'Back', None),
+ ('NodeSocketMaterial', 'BlackGlass', None),
+ ('NodeSocketMaterial', 'Glass', None),
+ ])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Depth"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ cube = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["PanelWidth"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Height"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"], 'Y': subtract, 'Z': subtract_1})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["MarginZ"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ cube_1 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_1, 'Pos': scale.outputs["Vector"]})
+
+ difference = nw.new_node(Nodes.MeshBoolean, input_kwargs={'Mesh 1': cube, 'Mesh 2': cube_1})
+
+ cube_2 = nw.new_node(nodegroup_cube().name,
+ input_kwargs={'Size': (0.0300, 0.0300, 0.0100), 'Pos': (0.1000, 0.0000, 0.0500), 'Resolution': 2})
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': cube_2})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance_1, 'Amount': 10},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: 0.0400},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz_7})
+
+ duplicate_elements_1 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': set_position_1, 'Amount': 7},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: 0.0200},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements_1.outputs["Geometry"], 'Offset': combine_xyz_8})
+
+ difference_1 = nw.new_node(Nodes.MeshBoolean,
+ input_kwargs={'Mesh 1': difference.outputs["Mesh"], 'Mesh 2': [duplicate_elements_1.outputs["Geometry"], set_position_2]})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': difference_1.outputs["Mesh"], 'Material': group_input.outputs["Back"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["DoorThickness"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ cube_3 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_2, 'Pos': combine_xyz_3, 'Resolution': 10})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["PanelWidth"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["MarginZ"]}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: multiply_2})
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: add}, attrs={'operation': 'LESS_THAN'})
+
+ separate_geometry = nw.new_node(Nodes.SeparateGeometry,
+ input_kwargs={'Geometry': cube_3, 'Selection': less_than},
+ attrs={'domain': 'FACE'})
+
+ convex_hull = nw.new_node(Nodes.ConvexHull, input_kwargs={'Geometry': separate_geometry.outputs["Selection"]})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': convex_hull, 'Level': 0})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ center = nw.new_node(nodegroup_center().name,
+ input_kwargs={'Geometry': subdivide_mesh, 'Vector': position_1, 'MarginX': -1.0000, 'MarginZ': group_input.outputs["DoorMargin"]})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': subdivide_mesh, 'Selection': center.outputs["In"], 'Material': group_input.outputs["BlackGlass"]})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_3, 'Selection': center.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ bounding_box_1 = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': subdivide_mesh})
+
+ add_2 = nw.new_node(Nodes.VectorMath, input_kwargs={0: bounding_box_1.outputs["Min"], 1: bounding_box_1.outputs["Max"]})
+
+ scale_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: add_2.outputs["Vector"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz_3 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale_1.outputs["Vector"]})
+
+ separate_xyz_4 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box_1.outputs["Min"]})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_4.outputs["Z"], 1: group_input.outputs["DoorMargin"]})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': separate_xyz_3.outputs["Y"], 'Z': add_3})
+
+ text = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_5, 'String': group_input.outputs["BrandName"], 'Size': 0.0300, 'Offset Scale': 0.0020})
+
+ text = complete_no_bevel(nw, text, preprocess)
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_2, text]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_1})
+
+ z = nw.scalar_multiply(group_input.outputs["DoorRotation"], 1 if not preprocess else 0)
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': z})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': geometry_to_instance, 'Rotation': combine_xyz_6, 'Pivot Point': combine_xyz_3})
+
+ plate = nw.new_node(nodegroup_plate().name, input_kwargs={'Scale': (0.1000, 0.1000, 0.1000)})
+
+ multiply_add = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: combine_xyz_1, 1: (0.5000, 0.5000, 0.0000), 2: scale.outputs["Vector"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': plate, 'Offset': multiply_add.outputs["Vector"]})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_position, 'Material': group_input.outputs["Glass"]})
+
+ convex_hull_1 = nw.new_node(Nodes.ConvexHull, input_kwargs={'Geometry': separate_geometry.outputs["Inverted"]})
+
+ subdivide_mesh_1 = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': convex_hull_1, 'Level': 0})
+
+ position_2 = nw.new_node(Nodes.InputPosition)
+
+ center_1 = nw.new_node(nodegroup_center().name,
+ input_kwargs={'Geometry': subdivide_mesh_1, 'Vector': position_2, 'MarginX': -1.0000, 'MarginY': 0.0010, 'MarginZ': group_input.outputs["DoorMargin"]})
+
+ set_material_4 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': subdivide_mesh_1, 'Selection': center_1.outputs["In"], 'Material': group_input.outputs["BlackGlass"]})
+
+ set_material_5 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_4, 'Selection': center_1.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': subdivide_mesh_1})
+
+ add_5 = nw.new_node(Nodes.VectorMath, input_kwargs={0: bounding_box.outputs["Min"], 1: bounding_box.outputs["Max"]})
+
+ scale_2 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: add_5.outputs["Vector"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale_2.outputs["Vector"]})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Max"]})
+
+ subtract_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: group_input.outputs["DoorMargin"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_3, 1: -0.1000})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': separate_xyz_1.outputs["Y"], 'Z': add_6})
+
+ text_1 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_4, 'String': '12:01', 'Offset Scale': 0.0050})
+
+ text_1 = complete_no_bevel(nw, text_1, preprocess)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [set_material_1, rotate_instances, set_material, set_material_5, text_1]})
+ geometry =nw.new_node(Nodes.RealizeInstances,[join_geometry])
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py
new file mode 100644
index 000000000..16eeff2da
--- /dev/null
+++ b/infinigen/assets/appliances/oven.py
@@ -0,0 +1,1215 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+
+from infinigen.assets.utils.misc import generate_text
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core.util.blender import delete
+from infinigen.core.util.bevelling import get_bevel_edges, add_bevel, complete_bevel, complete_no_bevel
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.material_assignments import AssetList
+from infinigen.assets.utils.object import new_bbox
+from infinigen.core.surface import write_attr_data
+from infinigen.assets.utils.decorate import read_normal
+from infinigen.core.tagging import PREFIX
+from infinigen.core import tagging, tags as t
+
+class OvenFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]):
+ super(OvenFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ with FixedSeed(factory_seed):
+ self.params, self.geometry_node_params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+ self.geometry_node_params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['OvenFactory']()
+ params = {
+ "Surface": material_assignments['surface'].assign_material(),
+ "Back": material_assignments['back'].assign_material(),
+ "WhiteMetal": material_assignments['white_metal'].assign_material(),
+ "SuperBlackGlass": material_assignments['black_glass'].assign_material(),
+ "Glass": material_assignments['glass'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # depth, width, height = dimensions
+ depth = 1 + N(0, 0.1)
+ width = 1 + N(0, 0.1)
+ height = 1 + N(0, 0.1)
+ door_thickness = U(0.05, 0.1) * depth
+ door_rotation = 0 # Set to 0 for now
+
+ rack_radius = U(0.01, 0.02) * depth
+ rack_h_amount = RI(2, 4)
+ rack_d_amount = RI(4, 6)
+
+ panel_height = U(0.2, 0.4) * height
+ panel_thickness = U(0.15, 0.25) * depth
+ botton_amount = RI(1, 3) * 2
+ botton_radius = U(0.05, 0.1) * width
+ botton_thickness = U(0.02, 0.04) * depth
+ heat_radius_ratio = U(0.1, 0.2)
+ brand_name = generate_text()
+
+ use_gas = RI(2)
+ n_grids = RI(2, 5)
+ grids = [RI(1, 4) for i in range(n_grids)]
+ branches = 2 * RI(2, 9)
+ grate_thickness = U(0.01, 0.03)
+ center_ratio = U(0.05, 0.15)
+ middle_ratio = U(0.5, 0.7)
+
+ params = {
+ "UseGas": use_gas,
+ "Grids": grids,
+ "Branches": branches,
+ "GrateThickness": grate_thickness,
+ "CenterRatio": center_ratio,
+ "MiddleRatio": middle_ratio,
+ "Depth": depth,
+ "Width": width,
+ "Height": height,
+ "DoorThickness": door_thickness,
+ "DoorRotation": door_rotation,
+ "RackRadius": rack_radius,
+ "RackHAmount": rack_h_amount,
+ "RackDAmount": rack_d_amount,
+ "PanelHeight": panel_height,
+ "PanelThickness": panel_thickness,
+ "BottonAmount": botton_amount,
+ "BottonRadius": botton_radius,
+ "BottonThickness": botton_thickness,
+ "HeaterRadiusRatio": heat_radius_ratio,
+ "BrandName": brand_name,
+ }
+ geometry_node_params = {k: params[k] for k in params.keys() if k not in [
+ "UseGas",
+ "Grids",
+ "Branches",
+ "GrateThickness",
+ "CenterRatio",
+ "MiddleRatio",
+ ]}
+ return params, geometry_node_params
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ # x, y, z = self.params["Depth"], self.params["Width"], self.params["Height"]
+ # box = new_bbox(-x/2 - 0.05, x/2 + self.params["DoorThickness"] + 0.1, -y/2, y/2, 0, z + 0.1)
+ # tagging.tag_object(box, f'{PREFIX}{t.Subpart.SupportSurface.value}', read_normal(box)[:, -1] > .5)
+ # box_top = new_bbox(-x/2 - 0.05, -x/2 - 0.05 + self.params["PanelThickness"], -y/2, y/2, z + 0.1, z+ 0.1 + 0.5)
+ # box_top.rotation_euler[1] = -0.1
+ #box = butil.join_objects([box, box_top])
+ obj = butil.spawn_cube()
+ return butil.modify_mesh(obj, 'NODES', node_group=nodegroup_oven_geometry(use_gas=self.params["UseGas"], is_placeholder=True), ng_inputs=self.geometry_node_params, apply=True)
+
+ def create_asset(self, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_oven_geometry(preprocess=True, use_gas=self.params["UseGas"]), ng_inputs=self.geometry_node_params, apply=True)
+ bevel_edges = get_bevel_edges(obj)
+ delete(obj)
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_oven_geometry(use_gas=self.params["UseGas"]), ng_inputs=self.geometry_node_params, apply=True)
+ obj = add_bevel(obj, bevel_edges, offset=0.01)
+ if not self.params["UseGas"]: return obj
+ width, depth = self.params["Width"], self.params["Depth"] + 2 * self.params["DoorThickness"]
+ grate_width, grate_depth = width * 0.8, depth * 0.6
+ grate_thickness = self.params["GrateThickness"]
+ grates = gas_grates(width, depth, grate_width, grate_depth, self.params["Height"] + self.params["DoorThickness"] - grate_thickness, grate_thickness, self.params["Grids"], self.params["Branches"], self.params["CenterRatio"], self.params["MiddleRatio"])
+ grates.data.materials.append(self.geometry_node_params["WhiteMetal"])
+ obj.data.materials.append(self.geometry_node_params["Back"])
+ with butil.SelectObjects(obj):
+ obj.active_material_index = len(obj.material_slots) - 1
+ for i in range(len(obj.material_slots)): bpy.ops.object.material_slot_move(direction='UP')
+ hollow= butil.spawn_cube(
+ size=1,
+ location=(depth / 2, width / 2, self.params["Height"] + self.params["DoorThickness"]),
+ scale=(grate_depth + grate_thickness, grate_width + grate_thickness, grate_thickness * 2),
+ )
+ with butil.SelectObjects(hollow):
+ bpy.ops.object.modifier_add(type='BEVEL')
+ bpy.context.object.modifiers["Bevel"].segments = 8
+ bpy.context.object.modifiers["Bevel"].width = grate_thickness
+ bpy.ops.object.modifier_apply(modifier="Bevel")
+ with butil.SelectObjects(obj):
+ bpy.ops.object.modifier_add(type='BOOLEAN')
+ bpy.context.object.modifiers["Boolean"].object = hollow
+ bpy.context.object.modifiers["Boolean"].use_hole_tolerant = True
+ bpy.ops.object.modifier_apply(modifier="Boolean")
+ butil.delete(hollow)
+ butil.join_objects([obj, grates], check_attributes=True)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+def gas_grates(width, depth, grate_width, grate_depth, height, thickness, grids, branches, center_ratio, middle_ratio):
+ high_height = height + thickness * 0.9
+ grates = []
+ for i, n in enumerate(grids):
+ cubes = [
+ butil.spawn_cube(size=1, location=(depth / 2, grate_width / len(grids) * i + (width - grate_width) / 2 + thickness / 2, height), scale=(grate_depth + thickness, thickness, thickness), name=None),
+ butil.spawn_cube(size=1, location=(depth / 2, grate_width / len(grids) * (i+1) + (width - grate_width) / 2 - thickness / 2, height), scale=(grate_depth + thickness, thickness, thickness), name=None),
+ ]
+ for j in range(n+1):
+ cubes.append(butil.spawn_cube(
+ size=1,
+ location=(grate_depth / n * j + (depth - grate_depth) / 2, grate_width / len(grids) * (i+0.5) + (width - grate_width) / 2, high_height),
+ scale=(thickness, grate_width / len(grids), thickness),
+ ))
+ for j in range(n):
+ min_dist = min(grate_width / len(grids) / 2, grate_depth / n / 2)
+ line_len = max(grate_width / len(grids) / 2, grate_depth / n / 2) - min_dist
+ center_dist = min_dist * center_ratio
+ middle_dist = min_dist * middle_ratio
+ if grate_width / len(grids) / 2 > grate_depth / n / 2:
+ x_center, y_center = center_dist, line_len + center_dist
+ x_middle, y_middle = middle_dist, line_len + middle_dist
+ x_full, y_full = min_dist, line_len + min_dist
+ else:
+ x_center, y_center = center_dist + line_len, center_dist
+ x_middle, y_middle = middle_dist + line_len, middle_dist
+ x_full, y_full = min_dist + line_len, min_dist
+ center = (grate_depth / n * (j+0.5) + (depth - grate_depth) / 2), grate_width / len(grids) * (i+0.5) + (width - grate_width) / 2
+ for k in range(branches):
+ angle = 2 * np.pi / branches * k
+ x0, y0 = x_center * np.cos(angle), y_center * np.sin(angle)
+ x1, y1 = x_middle * np.cos(angle), y_middle * np.sin(angle)
+ location = center[0] + (x0 + x1) / 2, center[1] + (y0 + y1) / 2, high_height
+ scale = ((x0 - x1) ** 2 + (y0 - y1) ** 2) ** 0.5, thickness, thickness
+ actual_angle = np.arctan2(y1-y0, x1-x0)
+ obj = butil.spawn_cube(size=1, location=location, scale=scale)
+ bpy.context.object.rotation_euler[2] = actual_angle
+ cubes.append(obj)
+ x0, y0 = x1, y1
+ if x_full - abs(x0) < y_full - abs(y0):
+ x1, y1 = x_full * np.sign(x0), y0
+ else:
+ x1, y1 = x0, y_full * np.sign(y0)
+ location = center[0] + (x0 + x1) / 2, center[1] + (y0 + y1) / 2, high_height
+ scale = ((x0 - x1) ** 2 + (y0 - y1) ** 2) ** 0.5, thickness, thickness
+ actual_angle = np.arctan2(y1-y0, x1-x0)
+ obj = butil.spawn_cube(size=1, location=location, scale=scale)
+ bpy.context.object.rotation_euler[2] = actual_angle
+ cubes.append(obj)
+ grates.append(butil.spawn_cylinder(center_dist + thickness, thickness / 2, location=(center[0], center[1], height)))
+ obj = butil.boolean(cubes)
+ for i in range(1, len(cubes)):
+ butil.delete(cubes[i])
+ with butil.SelectObjects(obj):
+ bpy.ops.object.modifier_add(type='REMESH')
+ remesh_type = "VOXEL"
+ bpy.context.object.modifiers["Remesh"].mode = remesh_type
+ bpy.context.object.modifiers["Remesh"].voxel_size = 0.004
+ bpy.ops.object.modifier_apply(modifier="Remesh")
+ bpy.ops.object.modifier_add(type='SMOOTH')
+ bpy.context.object.modifiers["Smooth"].iterations = 8
+ bpy.context.object.modifiers["Smooth"].factor = 1
+ bpy.ops.object.modifier_apply(modifier="Smooth")
+ grates.append(obj)
+ obj = butil.boolean(grates)
+ for i in range(1, len(grates)):
+ butil.delete(grates[i])
+ return obj
+
+@node_utils.to_nodegroup('nodegroup_hollow_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_hollow_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2),
+ ('NodeSocketFloat', 'Thickness', 0.0000),
+ ('NodeSocketBool', 'Switch1', False),
+ ('NodeSocketBool', 'Switch2', False),
+ ('NodeSocketBool', 'Switch3', False),
+ ('NodeSocketBool', 'Switch4', False),
+ ('NodeSocketBool', 'Switch5', False),
+ ('NodeSocketBool', 'Switch6', False)])
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Size"]})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Thickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Thickness"], 'Y': subtract, 'Z': subtract_1})
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"]}, attrs={'operation': 'MULTIPLY'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Pos"]})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["X"]})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Size"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale.outputs["Vector"]})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': subtract_2})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_5})
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch3"], 14: transform_2})
+
+ subtract_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': subtract_3, 'Z': group_input.outputs["Thickness"]})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_2, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_4 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map', 3: cube_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': add_3, 'Z': subtract_4})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_4, 'Translation': combine_xyz_3})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch2"], 14: transform_1})
+
+ subtract_5 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': subtract_5, 'Z': group_input.outputs["Thickness"]})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': add_5, 'Z': add_6})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1})
+
+ switch = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch1"], 14: transform})
+
+ subtract_6 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_7 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Thickness"], 'Y': subtract_6, 'Z': subtract_7})
+
+ cube_3 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_6, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_5 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_3.outputs["Mesh"], 'Name': 'uv_map', 3: cube_3.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subtract_8 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["X"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ subtract_9 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_8, 'Y': add_7, 'Z': subtract_9})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_5, 'Translation': combine_xyz_7})
+
+ switch_3 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch4"], 14: transform_3})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': group_input.outputs["Thickness"], 'Z': separate_xyz.outputs["Z"]})
+
+ cube_4 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_9, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_4.outputs["Mesh"], 'Name': 'uv_map', 3: cube_4.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_8 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["X"], 1: separate_xyz_2.outputs["X"]})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["Y"], 1: multiply_1})
+
+ add_10 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_1.outputs["Z"], 1: separate_xyz_2.outputs["Z"]})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_8, 'Y': add_9, 'Z': add_10})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_8})
+
+ switch_4 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch5"], 14: transform_4})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': group_input.outputs["Thickness"], 'Z': separate_xyz.outputs["Z"]})
+
+ cube_5 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_10, 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_5.outputs["Mesh"], 'Name': 'uv_map', 3: cube_5.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ add_11 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["X"], 1: separate_xyz_1.outputs["X"]})
+
+ subtract_10 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ add_12 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_11, 'Y': subtract_10, 'Z': add_12})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_3, 'Translation': combine_xyz_11})
+
+ switch_5 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Switch6"], 14: transform_5})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [switch_2.outputs[6], switch_1.outputs[6], switch.outputs[6], switch_3.outputs[6], switch_4.outputs[6], switch_5.outputs[6]]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_o', singleton=False, type='GeometryNodeTree')
+def nodegroup_o(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': (0.0000, 0.0000, 0.0020)})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Size', 1.0000)])
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Size"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle_1.outputs["Curve"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': curve_to_mesh, 'Offset Scale': 0.0030})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': extrude_mesh.outputs["Mesh"]}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_heater', singleton=False, type='GeometryNodeTree')
+def nodegroup_heater(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': (0.0000, 0.0000, 0.0010)})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.0000),
+ ('NodeSocketFloat', 'radius_ratio', 0.2000),
+ ('NodeSocketFloat', 'arrangement_ratio', 0.5000),
+ ('NodeSocketShader', 'SuperBlackGlass', None)])
+
+ minimum = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["width"], 1: group_input.outputs["depth"]},
+ attrs={'operation': 'MINIMUM'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: minimum, 1: group_input.outputs["radius_ratio"]},
+ label='Multiply',
+ attrs={'operation': 'MULTIPLY'})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': multiply})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line_1, 'Profile Curve': curve_circle_1.outputs["Curve"], 'Fill Caps': True})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': curve_to_mesh_1, 'Material': group_input.outputs["SuperBlackGlass"]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': set_material})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: minimum, 1: group_input.outputs["arrangement_ratio"]},
+ label='Multiply',
+ attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: multiply_1}, attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'FLOOR'})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: multiply_1}, attrs={'operation': 'DIVIDE'})
+
+ floor_1 = nw.new_node(Nodes.Math, input_kwargs={0: divide_1}, attrs={'operation': 'FLOOR'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: floor, 1: floor_1}, attrs={'operation': 'MULTIPLY'})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': multiply_2},
+ attrs={'domain': 'INSTANCE'})
+
+ divide_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: floor_1}, attrs={'operation': 'DIVIDE'})
+
+ divide_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: floor},
+ attrs={'operation': 'DIVIDE'})
+
+ floor_2 = nw.new_node(Nodes.Math, input_kwargs={0: divide_3}, attrs={'operation': 'FLOOR'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: floor_2, 1: divide_2}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: divide_2, 2: multiply_3}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ divide_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: floor}, attrs={'operation': 'DIVIDE'})
+
+ modulo = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: floor},
+ attrs={'operation': 'MODULO'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: modulo, 1: divide_4}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add_1 = nw.new_node(Nodes.Math, input_kwargs={0: divide_4, 2: multiply_4}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add, 'Y': multiply_add_1})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': set_position}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_oven_rack', singleton=False, type='GeometryNodeTree')
+def nodegroup_oven_rack(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Height', 2.0000),
+ ('NodeSocketFloatDistance', 'Radius', 0.0200),
+ ('NodeSocketInt', 'Amount', 5)])
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': group_input.outputs["Width"], 'Height': group_input.outputs["Height"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_3, 'End': combine_xyz_4})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': curve_line})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': group_input.outputs["Amount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply_2, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz})
+
+ duplicate_elements_1 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': group_input.outputs["Amount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ divide_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply_4, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: divide_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements_1.outputs["Geometry"], 'Offset': combine_xyz_1})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [quadrilateral, set_position, set_position_1]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Radius"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': join_geometry, 'Profile Curve': curve_circle.outputs["Curve"], 'Fill Caps': True})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': curve_to_mesh}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_text', singleton=False, type='GeometryNodeTree')
+def nodegroup_text(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Translation', (1.5000, 0.0000, 0.0000)),
+ ('NodeSocketString', 'String', 'BrandName'),
+ ('NodeSocketFloatDistance', 'Size', 0.0500),
+ ('NodeSocketFloat', 'Offset Scale', 0.0020)])
+
+ string_to_curves = nw.new_node('GeometryNodeStringToCurves',
+ input_kwargs={'String': group_input.outputs["String"], 'Size': group_input.outputs["Size"]},
+ attrs={'align_y': 'BOTTOM_BASELINE', 'align_x': 'CENTER'})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': string_to_curves.outputs["Curve Instances"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh,
+ input_kwargs={'Mesh': fill_curve, 'Offset Scale': group_input.outputs["Offset Scale"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': extrude_mesh.outputs["Mesh"], 'Translation': group_input.outputs["Translation"], 'Rotation': (1.5708, 0.0000, 1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'length', 0.0000),
+ ('NodeSocketFloat', 'thickness', 0.0200)])
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["width"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map', 3: cube_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["length"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [store_named_attribute, transform]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz_3})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"], 1: group_input.outputs["width"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': add, 'Z': group_input.outputs["thickness"]})
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"]}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: multiply_2})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': add_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_2})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_center', singleton=False, type='GeometryNodeTree')
+def nodegroup_center(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketVector', 'Vector', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketFloat', 'MarginX', 0.5000),
+ ('NodeSocketFloat', 'MarginY', 0.0000),
+ ('NodeSocketFloat', 'MarginZ', 0.0000)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Vector"], 1: bounding_box.outputs["Min"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract.outputs["Vector"]})
+
+ greater_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bounding_box.outputs["Max"], 1: group_input.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': subtract_1.outputs["Vector"]})
+
+ greater_than_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: group_input.outputs["MarginX"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than, 1: greater_than_1})
+
+ greater_than_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN'})
+
+ greater_than_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: group_input.outputs["MarginY"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_1 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_2, 1: greater_than_3})
+
+ op_and_2 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and, 1: op_and_1})
+
+ greater_than_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ greater_than_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Z"], 1: group_input.outputs["MarginZ"]},
+ attrs={'operation': 'GREATER_THAN', 'use_clamp': True})
+
+ op_and_3 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: greater_than_4, 1: greater_than_5})
+
+ op_and_4 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_2, 1: op_and_3})
+
+ op_not = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and_4}, attrs={'operation': 'NOT'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'In': op_and_4, 'Out': op_not}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Size', (0.1000, 10.0000, 4.0000)),
+ ('NodeSocketVector', 'Pos', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Resolution', 2)])
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': group_input.outputs["Size"], 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"], 'Vertices Z': group_input.outputs["Resolution"]})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map', 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Name': 'uv_map'},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ multiply_add = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Size"], 1: (0.5000, 0.5000, 0.5000), 2: group_input.outputs["Pos"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': multiply_add.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_oven_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_oven_geometry(nw: NodeWrangler, preprocess: bool=False, use_gas: bool=False, is_placeholder: bool=False):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Depth', 1.0000),
+ ('NodeSocketFloat', 'Width', 1.0000),
+ ('NodeSocketFloat', 'Height', 1.0000),
+ ('NodeSocketFloat', 'DoorThickness', 0.0700),
+ ('NodeSocketFloat', 'DoorRotation', 0.0000),
+ ('NodeSocketFloatDistance', 'RackRadius', 0.0100),
+ ('NodeSocketInt', 'RackHAmount', 2),
+ ('NodeSocketInt', 'RackDAmount', 5),
+ ('NodeSocketFloat', 'PanelHeight', 0.3000),
+ ('NodeSocketFloat', 'PanelThickness', 0.2000),
+ ('NodeSocketInt', 'BottonAmount', 4),
+ ('NodeSocketFloatDistance', 'BottonRadius', 0.0500),
+ ('NodeSocketFloat', 'BottonThickness', 0.0300),
+ ('NodeSocketFloat', 'HeaterRadiusRatio', 0.1500),
+ ('NodeSocketString', 'BrandName', 'BrandName'),
+ ('NodeSocketMaterial', 'Glass', None),
+ ('NodeSocketMaterial', 'Surface', None),
+ ('NodeSocketMaterial', 'WhiteMetal', None),
+ ('NodeSocketMaterial', 'SuperBlackGlass', None),
+ ('NodeSocketMaterial', 'Back', None),
+ ('NodeSocketBool', 'is_placeholder', is_placeholder)])
+
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["DoorThickness"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ cube = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_1, 'Pos': combine_xyz_2})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ center = nw.new_node(nodegroup_center().name,
+ input_kwargs={'Geometry': cube, 'Vector': position, 'MarginX': -1.0000, 'MarginY': 0.1000, 'MarginZ': 0.1500})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube, 'Selection': center.outputs["In"], 'Material': group_input.outputs["Glass"]})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_2, 'Selection': center.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+ # set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_3})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.8000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ handle = nw.new_node(nodegroup_handle().name,
+ input_kwargs={'width': multiply, 'length': multiply_1, 'thickness': multiply_2})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_3, 1: multiply_4})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.9200}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_13 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': multiply_5})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': handle, 'Translation': combine_xyz_13, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ set_material_8 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_1, 'Material': group_input.outputs["WhiteMetal"]})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': multiply_6, 'Z': 0.0300})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ text = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_12, 'String': group_input.outputs["BrandName"], 'Size': multiply_7})
+
+ text = complete_no_bevel(nw, text, preprocess)
+
+ set_material_9 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': text, 'Material': group_input.outputs["WhiteMetal"]})
+
+ set_material_8 = complete_bevel(nw, set_material_8, preprocess)
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_3, set_material_8, set_material_9]})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3})
+
+ y = nw.scalar_multiply(group_input.outputs["DoorRotation"], 1 if not preprocess else 0)
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': y})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Depth"]})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': geometry_to_instance, 'Rotation': combine_xyz_3, 'Pivot Point': combine_xyz_4})
+
+ rotate_instances = nw.new_node(Nodes.RealizeInstances, [rotate_instances])
+
+ door = nw.new_node(Nodes.Reroute, input_kwargs={'Input': rotate_instances}, label='door')
+
+ multiply_8 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: multiply_8},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_9 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: multiply_9},
+ attrs={'operation': 'SUBTRACT'})
+
+ ovenrack = nw.new_node(nodegroup_oven_rack().name,
+ input_kwargs={'Width': subtract, 'Height': subtract_1, 'Radius': group_input.outputs["RackRadius"], 'Amount': group_input.outputs["RackDAmount"]})
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': ovenrack})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance_1, 'Amount': group_input.outputs["RackHAmount"]},
+ attrs={'domain': 'INSTANCE'})
+
+ multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_11 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: 1.0000})
+
+ multiply_12 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["DoorThickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Height"], 1: multiply_12},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["RackHAmount"], 1: 1.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: add_4}, attrs={'operation': 'DIVIDE'})
+
+ multiply_13 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_10, 'Y': multiply_11, 'Z': multiply_13})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"], 'Offset': combine_xyz_5})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_position, 'Material': group_input.outputs["Surface"]})
+
+ set_material = nw.new_node(Nodes.RealizeInstances, [set_material])
+
+ racks = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material}, label='racks')
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]})
+
+ reroute_10 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': add_5})
+
+ reroute_11 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Width"]})
+
+ reroute_8 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["DoorThickness"]})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_10, 'Y': reroute_11, 'Z': reroute_8})
+
+ reroute_9 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Height"]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': reroute_9})
+
+ cube_1 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_6, 'Pos': combine_xyz_7})
+
+ set_material_5 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube_1, 'Material': group_input.outputs["Back"]})
+
+ # set_shade_smooth_1 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_5})
+
+ subtract_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: reroute_10, 1: group_input.outputs["PanelThickness"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["HeaterRadiusRatio"], 1: 2.0000, 2: 0.1000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ heater = nw.new_node(nodegroup_heater().name,
+ input_kwargs={'width': reroute_11, 'depth': subtract_3, 'radius_ratio': group_input.outputs["HeaterRadiusRatio"], 'arrangement_ratio': multiply_add})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_8, 1: reroute_9})
+
+ combine_xyz_15 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["PanelThickness"], 'Z': add_6})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': heater, 'Translation': combine_xyz_15})
+
+ transform_2 = complete_no_bevel(nw, transform_2, preprocess)
+
+ if use_gas:
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_5]})
+ else:
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_5, transform_2]})
+
+ heater_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': join_geometry_2}, label='heater')
+
+ reroute_14 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Width"]})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["PanelThickness"], 'Y': reroute_14, 'Z': group_input.outputs["PanelHeight"]})
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: group_input.outputs["DoorThickness"]})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add_7})
+
+ cube_2 = nw.new_node(nodegroup_cube().name, input_kwargs={'Size': combine_xyz_9, 'Pos': combine_xyz_8})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ center_1 = nw.new_node(nodegroup_center().name,
+ input_kwargs={'Geometry': cube_2, 'Vector': position_1, 'MarginX': -1.0000, 'MarginY': 0.0500, 'MarginZ': 0.0500})
+
+ set_material_4 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cube_2, 'Selection': center_1.outputs["In"], 'Material': group_input.outputs["Back"]})
+
+ set_material_7 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': set_material_4, 'Selection': center_1.outputs["Out"], 'Material': group_input.outputs["Surface"]})
+
+ # set_shade_smooth_3 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': set_material_7})
+
+ reroute_13 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["PanelThickness"]})
+
+ multiply_14 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_14}, attrs={'operation': 'MULTIPLY'})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': cube_2})
+
+ add_8 = nw.new_node(Nodes.VectorMath, input_kwargs={0: bounding_box.outputs["Min"], 1: bounding_box.outputs["Max"]})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: add_8.outputs["Vector"], 'Scale': 0.5000},
+ attrs={'operation': 'SCALE'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': scale.outputs["Vector"]})
+
+ combine_xyz_16 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_13, 'Y': multiply_14, 'Z': separate_xyz.outputs["Z"]})
+
+ multiply_15 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["PanelHeight"], 1: 0.2000},
+ attrs={'operation': 'MULTIPLY'})
+
+ text_1 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_16, 'String': '12:01', 'Size': multiply_15})
+
+ set_material_7 = complete_bevel(nw, set_material_7, preprocess)
+ text_1 = complete_no_bevel(nw, text_1, preprocess)
+
+ join_geometry_5 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_7, text_1]})
+
+ combine_xyz_21 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["BottonThickness"]})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_21})
+
+ reroute_12 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["BottonRadius"]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': reroute_12})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle.outputs["Curve"], 'Fill Caps': True})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_12, 1: 0.0050})
+
+ o = nw.new_node(nodegroup_o().name, input_kwargs={'Size': add_9})
+
+ join_geometry_4 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh, o]})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_13, 'Z': separate_xyz.outputs["Z"]})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_4, 'Translation': combine_xyz_10, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ reroute_16 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': separate_xyz.outputs["Z"]})
+
+ reroute_15 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["BottonRadius"]})
+
+ multiply_16 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["PanelHeight"], 1: 0.0500},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_add_1 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_15, 1: 1.0000, 2: multiply_16}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ add_10 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_16, 1: multiply_add_1})
+
+ combine_xyz_17 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_13, 'Z': add_10})
+
+ multiply_17 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["BottonRadius"], 1: 0.2500},
+ attrs={'operation': 'MULTIPLY'})
+
+ text_2 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_17, 'String': 'Off', 'Size': multiply_17})
+
+ multiply_add_2 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_15, 1: 0.7000, 2: multiply_16}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ add_11 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_16, 1: multiply_add_2})
+
+ combine_xyz_18 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_13, 'Y': multiply_add_2, 'Z': add_11})
+
+ text_3 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_18, 'String': 'High', 'Size': multiply_17})
+
+ multiply_18 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_16, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: reroute_15, 1: -0.7000, 2: multiply_18},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_19 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_13, 'Y': multiply_add_3, 'Z': add_11})
+
+ text_4 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_19, 'String': 'Low', 'Size': multiply_17})
+
+ add_12 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_13, 1: group_input.outputs["BottonThickness"]})
+
+ combine_xyz_20 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_12, 'Z': separate_xyz.outputs["Z"]})
+
+ multiply_19 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["BottonThickness"], 1: 0.1000},
+ attrs={'operation': 'MULTIPLY'})
+
+ text_5 = nw.new_node(nodegroup_text().name,
+ input_kwargs={'Translation': combine_xyz_20, 'String': '1', 'Size': group_input.outputs["BottonRadius"], 'Offset Scale': multiply_19})
+
+ join_geometry_6 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, text_2, text_3, text_4, text_5]})
+
+ geometry_to_instance_2 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_6})
+
+ add_13 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["BottonAmount"], 1: 2.0000})
+
+ reroute_6 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': add_13})
+
+ duplicate_elements_1 = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance_2, 'Amount': reroute_6},
+ attrs={'domain': 'INSTANCE'})
+
+ add_14 = nw.new_node(Nodes.Math, input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: 1.0000})
+
+ add_15 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_6, 1: 1.0000})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: add_15}, attrs={'operation': 'DIVIDE'})
+
+ multiply_20 = nw.new_node(Nodes.Math, input_kwargs={0: add_14, 1: divide_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_20})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': duplicate_elements_1.outputs["Geometry"], 'Offset': combine_xyz_11})
+
+ multiply_21 = nw.new_node(Nodes.Math, input_kwargs={0: add_13}, attrs={'operation': 'MULTIPLY'})
+
+ add_16 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_21, 1: -1.0100})
+
+ greater_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: add_16},
+ attrs={'operation': 'GREATER_THAN'})
+
+ add_17 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_21, 1: 0.9900})
+
+ less_than = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements_1.outputs["Duplicate Index"], 1: add_17},
+ attrs={'operation': 'LESS_THAN'})
+
+ minimum = nw.new_node(Nodes.Math, input_kwargs={0: greater_than, 1: less_than}, attrs={'operation': 'MINIMUM'})
+
+ delete_geometry = nw.new_node(Nodes.DeleteGeometry,
+ input_kwargs={'Geometry': set_position_1, 'Selection': minimum},
+ attrs={'domain': 'INSTANCE'})
+
+ set_material_6 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': delete_geometry, 'Material': group_input.outputs["WhiteMetal"]})
+
+ botton = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material_6}, label='botton')
+
+ botton = complete_no_bevel(nw, botton, preprocess)
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_5, botton]})
+
+ geometry_to_instance_3 = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_1})
+
+ combine_xyz_14 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Height"]})
+
+ panel_bbox = nw.new_node(Nodes.BoundingBox,input_kwargs={'Geometry': geometry_to_instance_3})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={'Switch': group_input.outputs["is_placeholder"], 'False': geometry_to_instance_3, 'True': panel_bbox})
+
+ rotate_instances_1 = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': switch_1, 'Rotation': (0.0000, -0.1745, 0.0000), 'Pivot Point': combine_xyz_14})
+
+ rotate_instances_1 = nw.new_node(Nodes.RealizeInstances, [rotate_instances_1])
+
+ panel = nw.new_node(Nodes.Reroute, input_kwargs={'Input': rotate_instances_1}, label='panel')
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Depth"], 'Y': group_input.outputs["Width"], 'Z': group_input.outputs["Height"]})
+
+ hollowcube = nw.new_node(nodegroup_hollow_cube().name,
+ input_kwargs={'Size': combine_xyz, 'Thickness': group_input.outputs["DoorThickness"], 'Switch2': True, 'Switch4': True})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': hollowcube, 'Material': group_input.outputs["Surface"]})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': set_material_1, 'Level': 0})
+
+ # set_shade_smooth_2 = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': subdivide_mesh})
+
+ body = nw.new_node(Nodes.Reroute, input_kwargs={'Input': subdivide_mesh}, label='Body')
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [door, racks, heater_1, panel, body]})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [door, racks, heater_1, body]})
+ body_bbox = nw.new_node(Nodes.BoundingBox,input_kwargs={'Geometry': join_geometry_2})
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [body_bbox, panel]})
+
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={'Switch': group_input.outputs["is_placeholder"], 'False': join_geometry, 'True': join_geometry_3})
+ geometry = nw.new_node(Nodes.RealizeInstances,[switch_2])
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': geometry})
diff --git a/infinigen/assets/appliances/tv.py b/infinigen/assets/appliances/tv.py
new file mode 100644
index 000000000..2a1dcebc6
--- /dev/null
+++ b/infinigen/assets/appliances/tv.py
@@ -0,0 +1,221 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix rotation
+
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_co, write_attribute, write_co, read_area, mirror, read_normal
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj, new_bbox, new_cube, new_plane
+from infinigen.assets.utils.uv import compute_uv_direction, face_corner2faces, unwrap_faces
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import read_attr_data, write_attr_data
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+from infinigen.assets.materials.text import Text
+
+
+class TVFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(TVFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.aspect_ratio = np.random.choice([9 / 16, 3 / 4])
+ self.width = uniform(0.6, 2.1)
+ self.screen_bevel_width = uniform(0, .01)
+ self.side_margin = log_uniform(.005, .01)
+ self.bottom_margin = uniform(.005, .03)
+ self.depth = uniform(.02, .04)
+ self.has_depth_extrude = uniform() < .4
+ if self.has_depth_extrude:
+ self.depth_extrude = self.depth * uniform(2, 5)
+ else:
+ self.depth_extrude = self.depth * 1.5
+ self.leg_type = np.random.choice(['two-legged', 'single-legged']) # 'none',
+ self.leg_length = uniform(.1, .2)
+ self.leg_length_y = uniform(.1, .15)
+ self.leg_radius = uniform(.008, .015)
+ self.leg_width = uniform(.5, .8)
+ self.leg_bevel_width = uniform(.01, .02)
+
+ materials = self.get_material_params()
+ self.surface = materials['surface']
+ self.scratch = materials['scratch']
+ self.edge_wear = materials['edge_wear']
+ self.screen_surface = materials['screen_surface']
+ self.support_surface = materials['support']
+
+ def get_material_params(self):
+ material_assignments = AssetList['TVFactory']()
+ surface = material_assignments['surface'].assign_material()
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ args = (self.factory_seed, False)
+ kwargs = {'emission': 0.01 if uniform() < 0.1 else uniform(2, 3)}
+ screen_surface = material_assignments['screen_surface'].assign_material()
+ if screen_surface == Text:
+ screen_surface = screen_surface(*args, **kwargs)
+ support = material_assignments['support'].assign_material()
+ return {
+ 'surface': surface, 'scratch': scratch, 'edge_wear': edge_wear, 'screen_surface': screen_surface,
+ 'support': support
+ }
+
+ @property
+ def height(self):
+ return self.aspect_ratio * self.width
+
+ @property
+ def total_width(self):
+ return self.width + 2 * self.side_margin
+
+ @property
+ def total_height(self):
+ return self.height + self.side_margin + self.bottom_margin
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ match self.leg_type:
+ case 'two-legged':
+ max_x = self.leg_length_y / 2 - (1 - self.leg_width) * self.depth_extrude
+ case _:
+ max_x = self.leg_length_y / 2 - self.depth_extrude / 2
+ return new_bbox(
+ - self.depth_extrude - self.depth, max_x, -self.total_width / 2,
+ self.total_width / 2, -self.leg_length - self.leg_radius / 2, self.total_height
+ )
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_base()
+ self.make_screen(obj)
+ parts = [obj]
+ match self.leg_type:
+ case 'two-legged':
+ legs = self.add_two_legs()
+ case _:
+ legs = self.add_single_leg()
+ for l in legs:
+ write_attribute(l, 1, 'leg', 'FACE', 'INT')
+ parts.extend(legs)
+ obj = join_objects(parts)
+ obj.rotation_euler[2] = np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def make_screen(self, obj):
+ cutter = new_cube()
+ cutter.location = 0, -1, 1
+ butil.apply_transform(cutter, True)
+ cutter.scale = self.width / 2, 1, self.height / 2
+ cutter.location = 0, 1e-3, self.bottom_margin
+ butil.apply_transform(cutter, True)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+ areas = read_area(obj)
+ screen = np.zeros(len(areas), int)
+ y = read_normal(obj)[:, 1] < 0
+ screen[np.argmax(areas + 1e5 * y)] = 1
+ fc2f = face_corner2faces(obj)
+ unwrap_faces(obj, screen)
+ bbox = compute_uv_direction(obj, 'x', 'z', screen[fc2f])
+ write_attr_data(obj, 'screen', screen, domain='FACE', type='INT')
+ self.screen_surface.apply(obj, 'screen', bbox)
+
+ def make_base(self):
+ obj = new_cube()
+ obj.location = 0, 1, 1
+ butil.apply_transform(obj, True)
+ obj.scale = self.total_width / 2, self.depth / 2, self.total_height / 2
+ butil.apply_transform(obj)
+ butil.modify_mesh(obj, 'BEVEL', width=self.screen_bevel_width, segments=8)
+ if not self.has_depth_extrude:
+ return obj
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if f.normal[1] > .5]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_KEEP_BOUNDARY')
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ height_min, height_max = self.total_height * uniform(.1, .3), self.total_height * uniform(.5, .7)
+ width = self.total_width * uniform(.3, .6)
+ extra = new_plane()
+ extra.scale = width / 2, (height_max - height_min) / 2, 1
+ extra.rotation_euler[0] = -np.pi / 2
+ extra.location = 0, self.depth_extrude + self.depth, self.total_height / 2
+ obj = join_objects([obj, extra])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.bridge_edge_loops(number_cuts=32, profile_shape_factor=-uniform(.0, .4))
+ x, y, z = read_co(obj).T
+ z += (height_max + height_min - self.total_height) / 2 * np.clip(
+ y - self.depth, 0,
+ None
+ ) / self.depth_extrude
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def add_two_legs(self):
+ vertices = (-self.total_width / 2 * self.leg_width * uniform(0, .6), 0, self.total_height * uniform(.3, .5)), (
+ 0, 0, -self.leg_length), (
+ 0, self.leg_length_y / 2, -self.leg_length), (0, -self.leg_length_y / 2, -self.leg_length)
+ edges = (0, 1), (1, 2), (1, 3)
+ leg = mesh2obj(data2mesh(vertices, edges))
+ surface.add_geomod(leg, geo_radius, apply=True, input_args=[self.leg_radius, 16])
+ x, y, z = read_co(leg).T
+ write_co(leg, np.stack([x, y, np.maximum(z, -self.leg_length - self.leg_radius * uniform(.0, .6))], -1))
+ leg_ = deep_clone_obj(leg)
+ butil.select_none()
+ leg.location = self.total_width / 2 * self.leg_width, (1 - self.leg_width) * self.depth_extrude, 0
+ butil.apply_transform(leg, True)
+ mirror(leg_)
+ leg_.location = -self.total_width / 2 * self.leg_width, (1 - self.leg_width) * self.depth_extrude, 0
+ butil.apply_transform(leg_, True)
+ return [leg, leg_]
+
+ def add_single_leg(self):
+ leg = new_cube()
+ leg.location = 0, 1, 1
+ butil.apply_transform(leg, True)
+ leg.location = 0, self.depth_extrude / 2, -self.leg_length
+ leg.scale = [self.total_width * uniform(.05, .1), self.leg_radius,
+ (self.leg_length + self.total_height * uniform(.3, .5)) / 2]
+ butil.apply_transform(leg, True)
+ butil.modify_mesh(leg, 'BEVEL', width=self.leg_bevel_width, segments=8)
+ base = new_cube()
+ base.location = 0, self.depth_extrude / 2, -self.leg_length
+ base.scale = [self.total_width * uniform(.15, .3), self.leg_length_y / 2, self.leg_radius]
+ butil.apply_transform(base, True)
+ butil.modify_mesh(base, 'BEVEL', width=self.leg_bevel_width, segments=8)
+ return [leg, base]
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, selection='!screen', rough=True, metal_color='bw')
+ self.support_surface.apply(assets, selection='leg', rough=True, metal_color='bw')
+
+
+class MonitorFactory(TVFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(MonitorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = log_uniform(.4, .8)
+ self.leg_type = 'single-legged'
diff --git a/infinigen/assets/bathroom/__init__.py b/infinigen/assets/bathroom/__init__.py
new file mode 100644
index 000000000..ab167ac6d
--- /dev/null
+++ b/infinigen/assets/bathroom/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .bathtub import BathtubFactory
+from .bathroom_sink import BathroomSinkFactory, StandingSinkFactory
+from .hardware import HardwareFactory
+from .toilet import ToiletFactory
diff --git a/infinigen/assets/bathroom/bathroom_sink.py b/infinigen/assets/bathroom/bathroom_sink.py
new file mode 100644
index 000000000..c1474d254
--- /dev/null
+++ b/infinigen/assets/bathroom/bathroom_sink.py
@@ -0,0 +1,143 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.bathroom import BathtubFactory
+from infinigen.assets.table_decorations import TapFactory
+from infinigen.assets.utils.decorate import read_co, subdivide_edge_ring, subsurf
+from infinigen.assets.utils.object import join_objects, new_base_cylinder, new_bbox, new_cube, origin2lowest
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+
+class BathroomSinkFactory(BathtubFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super(BathroomSinkFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.width = uniform(.6, .9)
+ self.size = self.width * log_uniform(.55, .8)
+ self.depth = self.width * log_uniform(.2, .4)
+ self.contour_fn = self.make_box_contour
+ self.sink_types = np.random.choice(['undermount', 'drop-in', 'vessel'])
+ self.has_stand = False
+ match self.sink_types:
+ case 'undermount':
+ self.bathtub_type = 'freestanding'
+ self.has_extrude = uniform() < .7
+ case 'drop-in':
+ self.bathtub_type = 'alcove'
+ self.has_extrude = True
+ case _:
+ self.bathtub_type = np.random.choice(['alcove', 'freestanding'])
+ self.has_extrude = uniform() < .7
+ self.has_stand = True
+ self.tap_factory = TapFactory(self.factory_seed)
+ self.disp_x = [self.disp_x[0], self.disp_x[0]]
+ self.alcove_levels = 0 if uniform() < .5 else np.random.randint(2, 4)
+ self.thickness = .01 if self.has_base else uniform(.01, .03)
+ self.size_extrude = uniform(.2, .35)
+ self.tap_offset = uniform(.0, .05)
+ self.stand_radius = self.width / 2 * log_uniform(.15, .2)
+ self.stand_bottom = self.width * log_uniform(.2, .3) if uniform() < .6 else self.stand_radius
+ self.stand_height = uniform(.7, .9) - self.depth
+ self.is_stand_circular = uniform() < .5
+ self.is_hole_centered = True
+ material_assignments = AssetList['BathroomSinkFactory']()
+ self.surface = material_assignments["surface"].assign_material()
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(
+ -(self.size_extrude + 1) * self.size, 0, 0, self.width,
+ -self.stand_height if self.has_stand else 0, self.depth
+ )
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ if self.has_base:
+ obj = self.make_base()
+ cutter = self.make_cutter()
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+ else:
+ obj = self.make_bowl()
+ self.remove_top(obj)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ subsurf(obj, self.side_levels)
+ obj.location = np.array(obj.location) - np.min(read_co(obj), 0)
+ butil.apply_transform(obj, True)
+ obj.scale = np.array([self.width, self.size, self.depth]) / np.array(obj.dimensions)
+ butil.apply_transform(obj, True)
+ if self.has_extrude:
+ self.extrude_back(obj)
+ if self.has_stand:
+ self.add_stand(obj)
+ hole = self.add_hole(obj)
+ obj = join_objects([obj, hole])
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj, True)
+ self.surface.apply(obj, clear=True, metal_color='plain')
+ if self.has_extrude:
+ tap = self.tap_factory(np.random.randint(1e7))
+ min_x = np.min(read_co(tap)[:, 0])
+ tap.location = (-1 - self.size_extrude + self.tap_offset) * self.size - min_x, self.width / 2, self.depth
+ butil.apply_transform(tap, True)
+ obj = join_objects([obj, tap])
+ return obj
+
+ def extrude_back(self, obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bm = bmesh.from_edit_mesh(obj.data)
+ for f in bm.faces:
+ f.select_set(f.calc_center_median()[1] > self.size / 2 and f.normal[1] > .1)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.extrude_region_move(
+ TRANSFORM_OT_translate={'value': (0, self.size_extrude * self.size, 0)}
+ )
+
+ def add_stand(self, obj):
+ if self.is_stand_circular:
+ stand = new_base_cylinder(vertices=16)
+ else:
+ stand = new_cube()
+ stand.scale = self.stand_radius, self.stand_radius, self.stand_height / 2
+ stand.location = self.width / 2, self.size / 2, -self.stand_height / 2
+ butil.apply_transform(stand, True)
+ subdivide_edge_ring(stand, np.random.randint(3, 6))
+ with butil.ViewportMode(stand, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bm = bmesh.from_edit_mesh(stand.data)
+ for f in bm.faces:
+ f.select_set(f.normal[-1] < -.1)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(stand.data)
+ bpy.ops.transform.resize(
+ value=(self.stand_bottom / self.stand_radius, self.stand_bottom / self.stand_radius, 1)
+ )
+ subsurf(stand, 2, True)
+ subsurf(stand, 1)
+ obj = join_objects([obj, stand])
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+class StandingSinkFactory(BathroomSinkFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(StandingSinkFactory, self).__init__(factory_seed, coarse)
+ self.bathtub_type = 'freestanding'
+ self.has_extrude = True
+ self.has_stand = True
diff --git a/infinigen/assets/bathroom/bathtub.py b/infinigen/assets/bathroom/bathtub.py
new file mode 100644
index 000000000..1d6df5012
--- /dev/null
+++ b/infinigen/assets/bathroom/bathtub.py
@@ -0,0 +1,289 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import (
+ read_center, read_co, read_normal, subsurf, write_attribute,
+ write_co,
+)
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import join_objects, new_bbox, new_cube, new_cylinder, new_line
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+
+from infinigen.assets.utils.autobevel import BevelSharp
+from infinigen.assets.material_assignments import AssetList
+
+
+class BathtubFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BathtubFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.width = uniform(1.5, 2)
+ self.size = uniform(.8, 1)
+ self.depth = uniform(.55, .7)
+ prob = np.array([2, 2])
+ self.bathtub_type = np.random.choice(['alcove', 'freestanding'], p=prob / prob.sum()) # , 'corner'
+ self.contour_fn = self.make_corner_contour if self.has_corner else self.make_box_contour
+ self.has_curve = uniform() < .5
+ self.has_legs = uniform() < .5
+
+ self.thickness = uniform(.04, .08) if self.has_base else uniform(.02, .04)
+ self.disp_x = uniform(0, .2, 2)
+ self.disp_y = uniform(0, .1)
+
+ self.leg_height = uniform(.2, .3) * self.depth
+ self.leg_side = uniform(.05, .1)
+ self.leg_radius = uniform(.02, .03)
+ self.leg_y_scale = uniform()
+ self.leg_subsurf_level = np.random.randint(3)
+
+ self.taper_factor = uniform(-.1, .1)
+ self.stretch_factor = uniform(-.2, .2)
+
+ self.alcove_levels = np.random.randint(1, 3) if self.has_base else 1
+ self.levels = 5
+ self.side_levels = 2
+
+ self.is_hole_centered = False
+ self.hole_radius = uniform(.015, .02)
+
+ # /////////////////// assign materials ///////////////////
+ material_assignments = AssetList['BathtubFactory']()
+ self.surface = material_assignments["surface"].assign_material()
+ self.leg_surface = material_assignments["leg"].assign_material()
+ self.hole_surface = material_assignments["hole"].assign_material()
+ is_scratch = uniform() < material_assignments["wear_tear_prob"][0]
+ is_edge_wear = uniform() < material_assignments["wear_tear_prob"][1]
+ self.scratch = material_assignments["wear_tear"][0] if is_scratch else None
+ self.edge_wear = material_assignments["wear_tear"][1] if is_edge_wear else None
+ # ////////////////////////////////////////////////////////
+
+ self.beveler = BevelSharp(mult=5, segments=5)
+
+ @property
+ def has_base(self):
+ return self.bathtub_type != 'freestanding'
+
+ @property
+ def has_corner(self):
+ return self.bathtub_type == 'corner'
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(-self.size, 0, 0, self.width, 0, self.depth)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ if self.has_base:
+ obj = self.make_base()
+ cutter = self.make_cutter()
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+ else:
+ obj = self.make_freestanding()
+ parts = [obj]
+ if self.has_legs:
+ parts.extend(self.make_legs(obj))
+ else:
+ parts.append(self.add_base(obj))
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ subsurf(obj, self.side_levels)
+ obj = join_objects(parts)
+ hole = self.add_hole(obj)
+ obj = join_objects([obj, hole])
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj, True)
+
+ if self.bathtub_type == 'freestanding':
+ butil.modify_mesh(obj, 'SUBSURF', levels=1, apply=True)
+ else:
+ self.beveler(obj)
+
+ return obj
+
+ def make_freestanding(self):
+ obj = self.make_bowl()
+ self.remove_top(obj)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.extrude_edges_move()
+ bpy.ops.transform.resize(
+ value=(1 + self.thickness * 2 / self.width, 1 + self.thickness / self.size, 1)
+ )
+ obj.location[1] -= self.size / 2
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'SIMPLE_DEFORM', deform_method='TAPER', angle=self.taper_factor)
+ butil.modify_mesh(obj, 'SIMPLE_DEFORM', deform_method='STRETCH', angle=self.taper_factor)
+ obj.location = 0, self.size / 2, -np.min(read_co(obj)[:, -1]) * uniform(.5, .7)
+ butil.apply_transform(obj, True)
+ return obj
+
+ def remove_top(self, obj):
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if f.calc_center_median()[-1] > self.depth]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_KEEP_BOUNDARY')
+ bmesh.update_edit_mesh(obj.data)
+
+ def make_legs(self, obj):
+ legs = []
+ co, normal = read_center(obj), read_normal(obj)
+ x, y, z = co.T
+ leg_height = np.min(z) + self.leg_height
+ for u in [1, -1]:
+ for v in [1, -1]:
+ metric = np.where(z < leg_height, u * x + v * y, -np.inf)
+ i = np.argmax(metric)
+ p = co[i]
+ n = normal[i]
+ q = co[i] + self.leg_side * np.array([n[0], n[1] * self.leg_y_scale, n[2]])
+ r = np.array([q[0], q[1], 0])
+ leg = new_line(2)
+ write_co(leg, np.stack([p, q, r]))
+ subsurf(leg, self.leg_subsurf_level)
+ surface.add_geomod(
+ leg, geo_radius, apply=True, input_args=[self.leg_radius, 32],
+ input_kwargs={'to_align_tilt': False}
+ )
+ butil.modify_mesh(leg, 'BEVEL', width=self.leg_radius * uniform(.3, .7))
+ leg.location[-1] = self.leg_radius
+ butil.apply_transform(leg, True)
+ write_attribute(leg, 1, 'leg', 'FACE')
+ legs.append(leg)
+ return legs
+
+ def add_base(self, obj):
+ obj = deep_clone_obj(obj)
+ cutter = new_cube()
+ x, y, z_ = read_co(obj).T
+ cutter.scale = 10, 10, np.min(z_) + self.leg_height
+ butil.apply_transform(cutter, True)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='INTERSECT')
+ butil.delete(cutter)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if len(f.verts) > 10]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_KEEP_BOUNDARY')
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.select_all(action='INVERT')
+ bpy.ops.mesh.delete(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, -self.depth)})
+ x, y, z = read_co(obj).T
+ z = np.clip(z, 0, None)
+ write_co(obj, np.stack([x, y, z], -1))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ subsurf(obj, 2)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ return obj
+
+ def make_box_contour(self, t, i):
+ return [(t + self.disp_x[0] * i, t + self.disp_y * i),
+ (self.width - t - self.disp_x[1] * i, t + self.disp_y * i),
+ (self.width - t - self.disp_x[1] * i, self.size - t - self.disp_y * i),
+ (t + self.disp_x[0] * i, self.size - t - self.disp_y * i)]
+
+ def make_corner_contour(self, t, i):
+ return [(t + self.disp_y * i, t + self.disp_y * i),
+ (self.width - t - self.disp_x[1] * i, t + self.disp_y * i),
+ (self.width - t - self.disp_x[1] * i, self.size - (t + self.disp_y * i) / np.sqrt(2)),
+ (self.size - (t + self.disp_y * i) / np.sqrt(2), self.width - t - self.disp_x[0] * i),
+ (t + self.disp_y * i, self.width - t - self.disp_x[0] * i)]
+
+ # noinspection PyArgumentList
+ def make_base(self):
+ contour = self.contour_fn(0, 0)
+ obj = new_cylinder(vertices=len(contour))
+ co = np.concatenate([np.array([[x, y, 0], [x, y, self.depth]]) for x, y in contour])
+ write_co(obj, co)
+ return obj
+
+ # noinspection PyArgumentList
+ def make_bowl(self):
+ if self.has_curve:
+ lower = self.contour_fn(0, 1)
+ upper = self.contour_fn(0, -1)
+ else:
+ lower = self.contour_fn(0, 0)
+ upper = self.contour_fn(0, 0)
+ obj = new_cylinder(vertices=len(lower))
+ co = np.concatenate(
+ [np.array([[x, y, 0], [z, w, self.depth * 2]]) for (x, y), (z, w) in zip(lower[::-1], upper[::-1])]
+ )
+ write_co(obj, co)
+ subsurf(obj, self.alcove_levels, True)
+ levels = self.levels - self.alcove_levels - self.side_levels
+ subsurf(obj, levels)
+ return obj
+
+ # noinspection PyArgumentList
+ def make_cutter(self):
+ if self.has_curve:
+ lower = self.contour_fn(self.thickness, 1)
+ upper = self.contour_fn(self.thickness, -1)
+ else:
+ lower = self.contour_fn(self.thickness, 0)
+ upper = self.contour_fn(self.thickness, 0)
+ obj = new_cylinder(vertices=len(lower))
+ co = np.concatenate(
+ [np.array([[x, y, self.thickness], [z, w, self.depth * 2 - self.thickness]]) for (x, y), (z, w) in
+ zip(lower[::-1], upper[::-1])]
+ )
+ write_co(obj, co)
+ subsurf(obj, self.alcove_levels, True)
+ levels = self.levels - self.alcove_levels
+ subsurf(obj, levels)
+ return obj
+
+ def find_hole(self, obj, x=None, y=None):
+ if x is None:
+ x = self.width / 2
+ if y is None:
+ y = self.size / 2
+ up_facing = read_normal(obj)[:, -1] > 0
+ center = read_center(obj)
+ i = np.argmin(np.abs(center[:, :2] - np.array([[x, y]])).sum(1) - up_facing)
+ return center[i]
+
+ def add_hole(self, obj):
+ match self.bathtub_type:
+ case 'alcove':
+ location = self.find_hole(obj)
+ case 'freestanding':
+ location = self.find_hole(obj, uniform(.35, .4) * self.width)
+ case _:
+ location = self.find_hole(obj, self.size / 2, self.size / 2)
+ if self.is_hole_centered:
+ location = self.find_hole(obj)
+ obj = new_cylinder()
+ obj.scale = self.hole_radius, self.hole_radius, .005
+ obj.location = location
+ butil.apply_transform(obj, True)
+ write_attribute(obj, 1, 'hole', 'FACE')
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, clear=True)
+ if self.has_legs and not self.has_base:
+ self.leg_surface.apply(assets, 'leg', metal_color='bw+natural')
+ self.hole_surface.apply(assets, 'hole', metal_color='bw+natural')
+
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/bathroom/hardware.py b/infinigen/assets/bathroom/hardware.py
new file mode 100644
index 000000000..411dcbbda
--- /dev/null
+++ b/infinigen/assets/bathroom/hardware.py
@@ -0,0 +1,121 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import subsurf
+from infinigen.assets.utils.object import join_objects, new_base_cylinder, new_cube
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+
+class HardwareFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(HardwareFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.attachment_radius = uniform(.02, .03)
+ self.attachment_depth = uniform(.01, .015)
+ self.radius = uniform(.01, .015)
+ self.depth = uniform(.06, .1)
+ self.is_circular = uniform() < .5
+ self.hardware_type = np.random.choice(['hook', 'holder', 'bar', 'ring'])
+ self.hook_length = self.attachment_radius * uniform(2, 4)
+ self.holder_length = uniform(.15, .25)
+ self.bar_length = uniform(.4, .8)
+ self.extension_length = self.attachment_radius * uniform(2, 3)
+ self.ring_radius = log_uniform(2, 6) * self.attachment_radius
+
+ material_assignments = AssetList['HardwareFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ is_scratch = uniform() < material_assignments['wear_tear_prob'][0]
+ is_edge_wear = uniform() < material_assignments['wear_tear_prob'][1]
+ self.scratch = material_assignments['wear_tear'][0] if is_scratch else None
+ self.edge_wear = material_assignments['wear_tear'][1] if is_edge_wear else None
+
+ def make_attachment(self):
+ base = new_base_cylinder() if self.is_circular else new_cube()
+ base.scale = self.attachment_radius, self.attachment_radius, self.attachment_depth / 2
+ base.rotation_euler[0] = np.pi / 2
+ base.location[1] = -self.attachment_depth / 2
+ butil.apply_transform(base, True)
+
+ rod = new_base_cylinder() if self.is_circular else new_cube()
+ rod.scale = self.radius, self.radius, self.depth / 2
+ rod.rotation_euler[0] = np.pi / 2
+ rod.location[1] = -self.depth / 2
+ butil.apply_transform(rod, True)
+ obj = join_objects([base, rod])
+ return obj
+
+ def make_hook(self):
+ obj = new_base_cylinder() if self.is_circular else new_cube()
+ obj.scale = self.radius, self.radius, self.hook_length / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def make_holder(self):
+ obj = new_base_cylinder() if self.is_circular else new_cube()
+ obj.scale = self.radius, self.radius, (self.holder_length + self.extension_length) / 2
+ obj.rotation_euler[1] = np.pi / 2
+ obj.location[0] = (self.holder_length - self.extension_length) / 2
+ butil.apply_transform(obj, True)
+ return obj
+
+ def make_bar(self):
+ obj = new_base_cylinder() if self.is_circular else new_cube()
+ obj.scale = self.radius, self.radius, self.bar_length / 2 + self.extension_length
+ obj.rotation_euler[1] = np.pi / 2
+ obj.location[0] = self.bar_length / 2
+ butil.apply_transform(obj, True)
+ return obj
+
+ def make_ring(self):
+ bpy.ops.mesh.primitive_torus_add(
+ major_segments=128, major_radius=self.ring_radius,
+ minor_radius=self.radius * uniform(.4, .7)
+ )
+ obj = bpy.context.active_object
+ obj.rotation_euler[0] = np.pi / 2
+ obj.location = 0, self.attachment_depth, -self.ring_radius
+ butil.apply_transform(obj, True)
+ subsurf(obj, 2)
+ return obj
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ match self.hardware_type:
+ case 'hook':
+ extra = self.make_hook()
+ case 'holder':
+ extra = self.make_holder()
+ case 'bar':
+ extra = self.make_bar()
+ case 'ring':
+ extra = self.make_ring()
+ case _:
+ return self.make_attachment()
+ extra.scale = [1 + 1e-3] * 3
+ extra.location[1] = -self.depth
+ butil.apply_transform(extra, True)
+ parts = [self.make_attachment(), extra]
+ if self.hardware_type == 'bar':
+ attachment_ = self.make_attachment()
+ attachment_.location[0] = self.bar_length
+ butil.apply_transform(attachment_, True)
+ parts.append(attachment_)
+ obj = join_objects(parts)
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, metal_color='plain')
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/bathroom/toilet.py b/infinigen/assets/bathroom/toilet.py
new file mode 100644
index 000000000..2e82f638e
--- /dev/null
+++ b/infinigen/assets/bathroom/toilet.py
@@ -0,0 +1,292 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import (
+ read_center, read_co, read_edge_center, read_edges, read_normal,
+ select_edges, select_faces, select_vertices, subsurf, write_attribute, write_co,
+)
+from infinigen.assets.utils.draw import align_bezier
+from infinigen.assets.utils.object import join_objects, new_bbox, new_cube, new_cylinder
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import normalize, FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+
+class ToiletFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.size = uniform(.4, .5)
+ self.width = self.size * uniform(.7, .8)
+ self.height = self.size * uniform(.8, .9)
+ self.size_mid = uniform(.6, .65)
+ self.curve_scale = log_uniform(.8, 1.2, 4)
+ self.depth = self.size * uniform(.5, .6)
+ self.tube_scale = uniform(.25, .3)
+ self.thickness = uniform(.05, .06)
+ self.extrude_height = uniform(.015, .02)
+ self.stand_depth = self.depth * uniform(.85, .95)
+ self.stand_scale = uniform(.7, .85)
+ self.bottom_offset = uniform(.5, 1.5)
+ self.back_thickness = self.thickness * uniform(0, .8)
+ self.back_size = self.size * uniform(.55, .65)
+ self.back_scale = uniform(.8, 1.)
+ self.seat_thickness = uniform(.1, .3) * self.thickness
+ self.seat_size = self.thickness * uniform(1.2, 1.6)
+ self.has_seat_cut = uniform() < .1
+ self.tank_width = self.width * uniform(1., 1.2)
+ self.tank_height = self.height * uniform(.6, 1.)
+ self.tank_size = self.back_size - self.seat_size - uniform(.02, .03)
+ self.tank_cap_height = uniform(.03, .04)
+ self.tank_cap_extrude = 0 if uniform() < .5 else uniform(.005, .01)
+ self.cover_rotation = - uniform(0, np.pi / 2)
+ self.hardware_type = np.random.choice(['button', 'handle'])
+ self.hardware_cap = uniform(.01, .015)
+ self.hardware_radius = uniform(.015, .02)
+ self.hardware_length = uniform(.04, .05)
+ self.hardware_on_side = uniform() < .5
+ material_assignments = AssetList['ToiletFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ self.hardware_surface = material_assignments['hardware_surface'].assign_material()
+
+ is_scratch = uniform() < material_assignments['wear_tear_prob'][0]
+ is_edge_wear = uniform() < material_assignments['wear_tear_prob'][1]
+ self.scratch = material_assignments['wear_tear'][0] if is_scratch else None
+ self.edge_wear = material_assignments['wear_tear'][1] if is_edge_wear else None
+
+ @property
+ def mid_offset(self):
+ return (1 - self.size_mid) * self.size
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(
+ -self.mid_offset - self.back_size - self.tank_cap_extrude,
+ self.size_mid * self.size + self.thickness + self.thickness,
+ -self.width / 2 - self.thickness * 1.1, self.width / 2 + self.thickness * 1.1, -self.height,
+ max(
+ self.tank_height,
+ -np.sin(self.cover_rotation) * (self.seat_size + self.size + self.thickness + self.thickness)
+ )
+ )
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ upper = self.build_curve()
+ lower = deep_clone_obj(upper)
+ lower.scale = [self.tube_scale] * 3
+ lower.location = 0, self.tube_scale * self.mid_offset / 2, -self.depth
+ butil.apply_transform(lower, True)
+ bottom = deep_clone_obj(upper)
+ bottom.scale = [self.stand_scale] * 3
+ bottom.location = 0, self.tube_scale * (
+ 1 - self.size_mid) * self.size / 2 * self.bottom_offset, -self.height
+ butil.apply_transform(bottom, True)
+
+ obj = self.make_tube(lower, upper)
+ seat, cover = self.make_seat(obj)
+ stand = self.make_stand(obj, bottom)
+ back = self.make_back(obj)
+ tank = self.make_tank()
+ butil.modify_mesh(obj, 'BEVEL', segments=2)
+ match self.hardware_type:
+ case 'button':
+ hardware = self.add_button()
+ case _:
+ hardware = self.add_handle()
+ write_attribute(hardware, 1, 'hardware', 'FACE')
+ obj = join_objects([obj, seat, cover, stand, back, tank, hardware])
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def build_curve(self):
+ x_anchors = [0, self.width / 2, 0]
+ y_anchors = [-self.size_mid * self.size, 0, self.mid_offset]
+ axes = [np.array([1, 0, 0]), np.array([0, 1, 0]), np.array([1, 0, 0])]
+ obj = align_bezier([x_anchors, y_anchors, 0], axes, self.curve_scale)
+ butil.modify_mesh(obj, 'MIRROR', use_axis=(True, False, False))
+ return obj
+
+ def make_tube(self, lower, upper):
+ obj = join_objects([upper, lower])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bridge_edge_loops(
+ number_cuts=np.random.randint(12, 16),
+ profile_shape_factor=uniform(.1, .2), interpolation='SURFACE'
+ )
+ butil.modify_mesh(
+ obj, 'SOLIDIFY', thickness=self.thickness, offset=1, solidify_mode='NON_MANIFOLD',
+ nonmanifold_boundary_mode='FLAT'
+ )
+ normal = read_normal(obj)
+ select_faces(obj, normal[:, -1] > .9)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.extrude_region_move(
+ TRANSFORM_OT_translate={'value': (0, 0, self.thickness + self.extrude_height)}
+ )
+ x, y, z = read_co(obj).T
+ write_co(obj, np.stack([x, y, np.clip(z, None, self.extrude_height)], -1))
+ return obj
+
+ def make_seat(self, obj):
+ seat = self.make_plane(obj)
+ cover = deep_clone_obj(seat)
+ butil.modify_mesh(seat, 'SOLIDIFY', thickness=self.extrude_height, offset=1)
+ if self.has_seat_cut:
+ cutter = new_cube()
+ cutter.scale = [self.thickness] * 3
+ cutter.location = 0, -self.thickness / 2 - self.size_mid * self.size, 0
+ butil.apply_transform(cutter, True)
+ butil.select_none()
+ butil.modify_mesh(seat, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+ butil.modify_mesh(seat, 'BEVEL', segments=2)
+
+ x, y, _ = read_edge_center(cover).T
+ i = np.argmin(np.abs(x) + np.abs(y))
+ selection = np.full(len(x), False)
+ selection[i] = True
+ select_edges(cover, selection)
+ with butil.ViewportMode(cover, 'EDIT'):
+ bpy.ops.mesh.loop_multi_select()
+ bpy.ops.mesh.fill_grid()
+ butil.modify_mesh(cover, 'SOLIDIFY', thickness=self.extrude_height, offset=1)
+ cover.location = [0, -self.mid_offset - self.seat_size + self.extrude_height / 2,
+ -self.extrude_height / 2]
+ butil.apply_transform(cover, True)
+ cover.rotation_euler[0] = self.cover_rotation
+ cover.location = [0, self.mid_offset + self.seat_size - self.extrude_height / 2,
+ self.extrude_height * 1.5]
+ butil.apply_transform(cover, True)
+ butil.modify_mesh(cover, 'BEVEL', segments=2)
+ return seat, cover
+
+ def make_plane(self, obj):
+ select_faces(obj, lambda x, y, z: z > self.extrude_height * 2 / 3)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+ seat = next(o for o in bpy.context.selected_objects if o != obj)
+ butil.select_none()
+ select_vertices(seat, lambda x, y, z: y > self.mid_offset + self.seat_thickness)
+ with butil.ViewportMode(seat, 'EDIT'):
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, self.seat_size + self.thickness * 2, 0)}
+ )
+ x, y, z = read_co(seat).T
+ write_co(seat, np.stack([x, np.clip(y, None, self.mid_offset + self.seat_size), z], -1))
+ return seat
+
+ def make_stand(self, obj, bottom):
+ co = read_co(obj)[read_edges(obj).reshape(-1)].reshape(-1, 2, 3)
+ horizontal = np.abs(normalize(co[:, 0] - co[:, 1])[:, -1]) < .1
+ x, y, z = read_edge_center(obj).T
+ under_depth = z < -self.stand_depth
+ i = np.argmin(y - horizontal - under_depth)
+ selection = np.full(len(co), False)
+ selection[i] = True
+ select_edges(obj, selection)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.loop_multi_select()
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+ stand = next(o for o in bpy.context.selected_objects if o != obj)
+ stand = join_objects([stand, bottom])
+ with butil.ViewportMode(stand, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bridge_edge_loops(
+ number_cuts=np.random.randint(12, 16),
+ profile_shape_factor=uniform(.0, .15)
+ )
+ return stand
+
+ def make_back(self, obj):
+ back = read_center(obj)[:, 1] > self.mid_offset - self.back_thickness
+ back_facing = read_normal(obj)[:, 1] > .1
+ butil.select_none()
+ select_faces(obj, back & back_facing)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+ back = next(o for o in bpy.context.selected_objects if o != obj)
+ butil.modify_mesh(back, 'CORRECTIVE_SMOOTH')
+ butil.select_none()
+ with butil.ViewportMode(back, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, self.back_size + self.thickness * 2, 0)}
+ )
+ bpy.ops.transform.resize(value=(self.back_scale, 1, 1))
+ bpy.ops.mesh.edge_face_add()
+ back.location[1] -= .01
+ butil.apply_transform(back, True)
+ x, y, z = read_co(back).T
+ write_co(back, np.stack([x, np.clip(y, None, self.mid_offset + self.back_size), z], -1))
+ return back
+
+ def make_tank(self):
+ tank = new_cube()
+ tank.scale = self.tank_width / 2, self.tank_size / 2, self.tank_height / 2
+ tank.location = 0, self.mid_offset + self.back_size - self.tank_size / 2, self.tank_height / 2
+ butil.apply_transform(tank, True)
+ subsurf(tank, 2, True)
+ butil.modify_mesh(tank, 'BEVEL', segments=2)
+ cap = new_cube()
+ cap.scale = self.tank_width / 2 + self.tank_cap_extrude, self.tank_size / 2 + self.tank_cap_extrude, \
+ self.tank_cap_height / 2
+ cap.location = 0, self.mid_offset + self.back_size - self.tank_size / 2, self.tank_height
+ butil.apply_transform(cap, True)
+ butil.modify_mesh(cap, 'BEVEL', width=uniform(0, self.extrude_height), segments=4)
+ tank = join_objects([tank, cap])
+ return tank
+
+ def add_button(self):
+ obj = new_cylinder()
+ obj.scale = self.hardware_radius, self.hardware_radius, self.tank_cap_height / 2 + 1e-3
+ obj.location = 0, self.mid_offset + self.back_size - self.tank_size / 2, self.tank_height
+ butil.apply_transform(obj, True)
+ return obj
+
+ def add_handle(self):
+ obj = new_cylinder()
+ obj.scale = self.hardware_radius, self.hardware_radius, self.hardware_cap
+ obj.rotation_euler[0] = np.pi / 2
+ butil.apply_transform(obj, True)
+ lever = new_cylinder()
+ lever.scale = self.hardware_radius / 2, self.hardware_radius / 2, self.hardware_length
+ lever.rotation_euler[1] = np.pi / 2
+ lever.location = [-self.hardware_radius * uniform(0, .5), -self.hardware_cap,
+ -self.hardware_radius * uniform(0, .5)]
+ butil.apply_transform(lever, True)
+ obj = join_objects([obj, lever])
+ if self.hardware_on_side:
+ obj.location = [-self.tank_width / 2 + self.hardware_radius + uniform(.01, .02),
+ self.mid_offset + self.back_size - self.tank_size,
+ self.tank_height - self.hardware_radius - uniform(.02, .03)]
+ else:
+ obj.location = [-self.tank_width / 2,
+ self.mid_offset + self.back_size - self.tank_size + self.hardware_radius + uniform(.01, .02),
+ self.tank_height - self.hardware_radius - uniform(.02, .03)]
+ obj.rotation_euler[-1] = -np.pi / 2
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'BEVEL', width=uniform(.005, .01), segments=2)
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, clear=True, metal_color='plain')
+ self.hardware_surface.apply(assets, 'hardware', metal_color='natural')
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/cactus/columnar.py b/infinigen/assets/cactus/columnar.py
index 177105eaa..84480da89 100644
--- a/infinigen/assets/cactus/columnar.py
+++ b/infinigen/assets/cactus/columnar.py
@@ -15,7 +15,7 @@
from infinigen.core import surface
from infinigen.assets.cactus.base import BaseCactusFactory
from infinigen.assets.trees.tree import build_radius_tree
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core import tagging
class ColumnarBaseCactusFactory(BaseCactusFactory):
@@ -75,7 +75,6 @@ def create_asset(self, face_size=.01, **params) -> bpy.types.Object:
surface.add_geomod(obj, self.geo_star, apply=True, input_attributes=[None, 'radius'],
attributes=['selection'])
surface.add_geomod(obj, geo_extension, apply=True, input_kwargs={'musgrave_dimensions': '2D'})
- tag_object(obj, 'columnar_cactus')
return obj
@staticmethod
diff --git a/infinigen/assets/cactus/generate.py b/infinigen/assets/cactus/generate.py
index 2ef7022cc..674afb418 100644
--- a/infinigen/assets/cactus/generate.py
+++ b/infinigen/assets/cactus/generate.py
@@ -16,23 +16,28 @@
from .columnar import ColumnarBaseCactusFactory
from .pricky_pear import PrickyPearBaseCactusFactory
from .kalidium import KalidiumBaseCactusFactory
+
from infinigen.assets.cactus import spike
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
-from infinigen.assets.utils.decorate import assign_material, join_objects
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects
+
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
from infinigen.core.placement.detail import remesh_with_attrs
from infinigen.core import surface
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object
+from infinigen.core import tagging
+from infinigen.core.nodes.node_utils import build_color_ramp
class CactusFactory(AssetFactory):
-
+
def __init__(self, factory_seed, coarse=False, factory_method=None):
super(CactusFactory, self).__init__(factory_seed, coarse)
with FixedSeed(factory_seed):
self.factory_methods = [GlobularBaseCactusFactory, ColumnarBaseCactusFactory,
- PrickyPearBaseCactusFactory, KalidiumBaseCactusFactory]
+ PrickyPearBaseCactusFactory]#, KalidiumBaseCactusFactory]
weights = np.array([1] * len(self.factory_methods))
self.weights = weights / weights.sum()
if factory_method is None:
@@ -44,6 +49,7 @@ def __init__(self, factory_seed, coarse=False, factory_method=None):
def create_asset(self, face_size=0.01, realize=True, **params):
obj = self.factory.create_asset(**params)
+
remesh_with_attrs(obj, face_size)
if self.factory.noise_strength > 0:
@@ -52,20 +58,25 @@ def create_asset(self, face_size=0.01, realize=True, **params):
texture.noise_scale = log_uniform(.1, .15)
butil.modify_mesh(obj, 'DISPLACE', True, strength=self.factory.noise_strength, mid_level=0,
texture=texture)
+
assign_material(obj, self.material)
+
if face_size <= .05 and self.factory.density > 0:
t = spike.apply(obj, self.factory.points_fn, self.factory.base_radius, realize)
+
+ tagging.tag_object(obj, "cactus_spike")
obj = join_objects([obj, t])
- tag_object(obj, 'cactus')
+ tagging.tag_object(obj, 'cactus')
+
return obj
@staticmethod
def shader_cactus(nw: NodeWrangler, base_hue):
shift = uniform(-.15, .15)
- bright_color = *colorsys.hsv_to_rgb((base_hue + shift) % 1, 1., .02), 1
- dark_color = *colorsys.hsv_to_rgb(base_hue, .8, .01), 1
- fresnel_color = *colorsys.hsv_to_rgb((base_hue - uniform(.05, .1)) % 1, .9, uniform(.3, .5)), 1
+ bright_color = hsv2rgba((base_hue + shift) % 1, 1., .02)
+ dark_color = hsv2rgba(base_hue, .8, .01)
+ fresnel_color = hsv2rgba((base_hue - uniform(.05, .1)) % 1, .9, uniform(.3, .5))
specular = .25
fresnel = nw.scalar_multiply(nw.new_node(Nodes.Fresnel), log_uniform(.6, 1.))
diff --git a/infinigen/assets/cactus/globular.py b/infinigen/assets/cactus/globular.py
index 0ccc5c516..b78785f93 100644
--- a/infinigen/assets/cactus/globular.py
+++ b/infinigen/assets/cactus/globular.py
@@ -12,12 +12,12 @@
from infinigen.assets.utils.object import new_cube
from infinigen.assets.utils.decorate import geo_extension
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core import tagging
class GlobularBaseCactusFactory(BaseCactusFactory):
@@ -46,11 +46,13 @@ def geo_globular(nw: NodeWrangler):
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry, 'Selection': selection})
def create_asset(self, face_size=.01, **params) -> bpy.types.Object:
+
obj = new_cube()
surface.add_geomod(obj, self.geo_globular, apply=True, attributes=['selection'])
surface.add_geomod(obj, geo_extension, apply=True, input_kwargs={'musgrave_dimensions': '2D'})
+
obj.scale = uniform(.8, 1.5, 3)
obj.rotation_euler[-1] = uniform(0, np.pi * 2)
butil.apply_transform(obj)
- tag_object(obj, 'globular_cactus')
+
return obj
diff --git a/infinigen/assets/cactus/kalidium.py b/infinigen/assets/cactus/kalidium.py
index 995e5225e..e0dc9c0e9 100644
--- a/infinigen/assets/cactus/kalidium.py
+++ b/infinigen/assets/cactus/kalidium.py
@@ -10,8 +10,8 @@
from infinigen.assets.trees.tree import TreeVertices, build_radius_tree, recursive_path
from infinigen.assets.utils.nodegroup import geo_radius
-from infinigen.assets.utils.object import data2mesh, mesh2obj, new_cube, origin2lowest
-from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, remove_vertices, separate_loose, \
+from infinigen.assets.utils.object import data2mesh, mesh2obj, new_cube, origin2lowest, separate_loose
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, remove_vertices, \
subsurface2face_size
from infinigen.assets.utils.shortest_path import geo_shortest_path
from infinigen.core.nodes.node_info import Nodes
@@ -21,7 +21,7 @@
from infinigen.core import surface
from infinigen.assets.cactus.base import BaseCactusFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class KalidiumBaseCactusFactory(BaseCactusFactory):
diff --git a/infinigen/assets/cactus/pricky_pear.py b/infinigen/assets/cactus/pricky_pear.py
index c7194cdee..8a74ae34f 100644
--- a/infinigen/assets/cactus/pricky_pear.py
+++ b/infinigen/assets/cactus/pricky_pear.py
@@ -8,16 +8,16 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.object import new_cube
-from infinigen.assets.utils.decorate import geo_extension, join_objects
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.assets.utils.object import join_objects, new_cube
+from infinigen.assets.utils.decorate import geo_extension
+from infinigen.core.util.random import log_uniform
from infinigen.core.surface import write_attr_data
from infinigen.core.util import blender as butil
from infinigen.assets.cactus.base import BaseCactusFactory
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class PrickyPearBaseCactusFactory(BaseCactusFactory):
spike_distance = .08
diff --git a/infinigen/assets/cactus/spike.py b/infinigen/assets/cactus/spike.py
index 055f952da..527d9a584 100644
--- a/infinigen/assets/cactus/spike.py
+++ b/infinigen/assets/cactus/spike.py
@@ -8,9 +8,9 @@
import numpy as np
from numpy.random import uniform
+from infinigen.core.util.color import hsv2rgba
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.misc import sample_direction
-from infinigen.assets.utils.decorate import assign_material
+from infinigen.assets.utils.misc import assign_material, sample_direction, toggle_show, toggle_hide
from infinigen.assets.utils.nodegroup import geo_radius
from infinigen.core.placement.factory import AssetFactory, make_asset_collection
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
@@ -18,7 +18,7 @@
from infinigen.assets.trees.tree import build_radius_tree
import infinigen.core.util.blender as butil
from infinigen.core.util.blender import deep_clone_obj
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup, COMBINED_ATTR_NAME
def build_spikes(base_radius=.002, **kwargs):
n_branch = 4
@@ -33,7 +33,6 @@ def build_spikes(base_radius=.002, **kwargs):
np.arange(size * resolution) / (size * resolution))
obj = build_radius_tree(radius_fn, branch_config, base_radius)
surface.add_geomod(obj, geo_radius, apply=True, input_args=['radius', None, .001])
- tag_object(obj, 'spike')
return obj
@@ -58,8 +57,15 @@ def selection(nw: NodeWrangler, selected, geometry):
def geo_spikes(nw: NodeWrangler, spikes, points_fn=None, realize=True):
- geometry, selection = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None),
- ('NodeSocketFloat', 'Selection', None)]).outputs[:2]
+
+ geometry, selection = nw.new_node(
+ Nodes.GroupInput,
+ expose_input=[
+ ('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketFloat', 'Selection', None)
+ ]
+ ).outputs[:2]
+
capture = nw.new_node(Nodes.CaptureAttribute,
input_kwargs={'Geometry': geometry, 'Value': nw.new_node(Nodes.InputNormal)})
@@ -86,7 +92,7 @@ def geo_spikes(nw: NodeWrangler, spikes, points_fn=None, realize=True):
realize_instances = nw.new_node(Nodes.RealizeInstances, [spikes])
else:
realize_instances = spikes
-
+
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances})
@@ -94,7 +100,7 @@ def shader_spikes(nw: NodeWrangler):
roughness = .8
specular = .25
mix_ratio = .9
- color = *colorsys.hsv_to_rgb(uniform(.2, .4), uniform(.1, .3), .8), 1
+ color = hsv2rgba(uniform(.2, .4), uniform(.1, .3), .8)
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
'Base Color': color,
'Roughness': roughness,
@@ -108,10 +114,16 @@ def shader_spikes(nw: NodeWrangler):
def apply(obj, points_fn, base_radius=.002, realize=True):
spikes = deep_clone_obj(obj)
+
+ if COMBINED_ATTR_NAME in spikes.data.attributes:
+ spikes.data.attributes.remove(spikes.data.attributes[COMBINED_ATTR_NAME])
+
instances = make_asset_collection(build_spikes, 5, 'spikes', verbose=False, base_radius=base_radius)
mat = surface.shaderfunc_to_material(shader_spikes)
+ toggle_show(instances)
for o in instances.objects:
- assign_material(o, mat)
+ assign_material(o, mat)
+ toggle_hide(instances)
surface.add_geomod(spikes, geo_spikes, apply=realize, input_args=[instances, points_fn, realize],
input_attributes=[None, 'selection'])
butil.delete_collection(instances)
diff --git a/infinigen/assets/clothes/__init__.py b/infinigen/assets/clothes/__init__.py
new file mode 100644
index 000000000..a1b27f179
--- /dev/null
+++ b/infinigen/assets/clothes/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .blanket import BlanketFactory
+from .shirt import ShirtFactory
+from .pants import PantsFactory
+from .towel import TowelFactory
+
diff --git a/infinigen/assets/clothes/blanket.py b/infinigen/assets/clothes/blanket.py
new file mode 100644
index 000000000..2d5d018b7
--- /dev/null
+++ b/infinigen/assets/clothes/blanket.py
@@ -0,0 +1,79 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import art, fabrics
+from infinigen.assets.utils.decorate import read_co, select_vertices, write_co
+from infinigen.assets.utils.object import new_grid
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.materials.art import ArtFabric
+from infinigen.assets.material_assignments import AssetList
+
+class BlanketFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BlanketFactory, self).__init__(factory_seed, coarse)
+ self.width = log_uniform(.9, 1.2)
+ self.size = self.width * log_uniform(.4, .7)
+ self.thickness = log_uniform(.004, .008)
+
+ materials = AssetList['BlanketFactory']()
+ self.surface = materials['surface'].assign_material()
+ if self.surface == ArtFabric:
+ self.surface = self.surface(self.factory_seed)
+
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_grid(x_subdivisions=64, y_subdivisions=int(self.size / self.width * 64))
+ obj.scale = self.width / 2, self.size / 2, 1
+ butil.apply_transform(obj)
+ unwrap_faces(obj)
+ self.surface.apply(obj)
+ return obj
+
+ def fold(self, obj):
+ theta = uniform(-np.pi / 6, np.pi / 6)
+ y_margin = self.size * (.5 - uniform(.1, .3))
+ obj.rotation_euler[-1] = theta
+ obj.location[1] -= y_margin
+ butil.apply_transform(obj, True)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bisect(plane_co=(0, 0, 0), plane_no=(0, 1, 0))
+ x, y, z = read_co(obj).T
+ co = np.stack([x, np.where(y > 0, -y, y), np.where(y > 0, .05 - z, z)], -1)
+ write_co(obj, co)
+ obj.location[1] += y_margin
+ butil.apply_transform(obj, True)
+ obj.rotation_euler[-1] = -theta
+ butil.apply_transform(obj)
+
+
+class ComforterFactory(BlanketFactory):
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = super().create_asset(**params)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=.01)
+ return obj
+
+
+class BoxComforterFactory(ComforterFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BoxComforterFactory, self).__init__(factory_seed, coarse)
+ self.margin = uniform(.3, .4)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = super().create_asset(**params)
+ x, y, _ = read_co(obj).T
+ _x = np.abs(x / self.margin - np.round(x / self.margin)) * self.margin < self.width / 64 / 2
+ _y = np.abs(y / self.margin - np.round(y / self.margin)) * self.margin < self.width / 64 / 2
+ with butil.ViewportMode(obj, 'EDIT'):
+ select_vertices(obj, _x | _y)
+ bpy.ops.mesh.remove_doubles(threshold=.02)
+ return obj
diff --git a/infinigen/assets/clothes/pants.py b/infinigen/assets/clothes/pants.py
new file mode 100644
index 000000000..848f94980
--- /dev/null
+++ b/infinigen/assets/clothes/pants.py
@@ -0,0 +1,66 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import art, fabrics
+from infinigen.assets.utils.decorate import distance2boundary, read_normal, remove_faces, subsurf, write_co
+from infinigen.assets.utils.draw import remesh_fill
+from infinigen.assets.utils.object import new_circle
+from infinigen.assets.utils.uv import unwrap_faces, wrap_front_back, wrap_top_bottom
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+from infinigen.assets.materials.art import ArtFabric
+
+
+class PantsFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(PantsFactory, self).__init__(factory_seed, coarse)
+ self.width = log_uniform(.45, .55)
+ self.size = self.width / 2 + uniform(0, .05)
+ self.type = np.random.choice(['underwear', 'shorts', 'pants'])
+ match self.type:
+ case 'underwear':
+ self.length = self.size + uniform(-.02, .02)
+ case 'shorts':
+ self.length = self.size + uniform(.05, .1)
+ case _:
+ self.length = self.size + uniform(.5, .7)
+ self.neck_shrink = uniform(.1, .15)
+ self.thickness = log_uniform(.02, .03)
+ materials = AssetList['PantsFactory']()
+ self.surface = materials['surface'].assign_material()
+ if self.surface == ArtFabric:
+ self.surface = self.surface(self.factory_seed)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = 0, self.width / 2, self.width / 2 * (
+ 1 + self.neck_shrink), self.width / 2 * self.neck_shrink * 2, 0
+ y_anchors = 0, 0, -self.length, -self.length, -self.size
+
+ obj = new_circle(vertices=len(x_anchors))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.edge_face_add()
+ write_co(obj, np.stack([x_anchors, y_anchors, np.zeros_like(x_anchors)], -1))
+ butil.modify_mesh(obj, 'MIRROR', use_axis=(True, False, False))
+ remesh_fill(obj, .02)
+ distance2boundary(obj)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=0)
+ x_, y_, z_ = read_normal(obj).T
+ remove_faces(obj, (y_ < -.99) | (y_ > .99))
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.remove_doubles(threshold=1e-3)
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_loose()
+ bpy.ops.mesh.delete(type='EDGE')
+ wrap_top_bottom(obj, self.surface)
+ subsurf(obj, 1)
+ return obj
diff --git a/infinigen/assets/clothes/shirt.py b/infinigen/assets/clothes/shirt.py
new file mode 100644
index 000000000..9c5b1c7ec
--- /dev/null
+++ b/infinigen/assets/clothes/shirt.py
@@ -0,0 +1,71 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_center, read_normal, remove_faces, subsurf, write_co
+from infinigen.assets.utils.draw import remesh_fill
+from infinigen.assets.utils.object import new_circle
+from infinigen.assets.utils.uv import wrap_front_back
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+from infinigen.assets.materials.art import ArtFabric
+
+class ShirtFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(ShirtFactory, self).__init__(factory_seed, coarse)
+ self.width = log_uniform(.45, .55)
+ self.size = self.width + uniform(.25, .3)
+ self.size_neck = uniform(.1, .15) * self.size
+ self.type = np.random.choice(['short', 'long'])
+ match self.type:
+ case 'short':
+ self.sleeve_length = self.size / 2 + uniform(-.35, -.3)
+ case _:
+ self.sleeve_length = self.size / 2 + uniform(-.05, .0)
+ self.sleeve_width = uniform(.14, .18)
+ self.sleeve_angle = uniform(np.pi / 6, np.pi / 4)
+ self.thickness = log_uniform(.02, .03)
+ materials = AssetList['ShirtFactory']()
+ self.surface = materials['surface'].assign_material()
+ if self.surface == ArtFabric:
+ self.surface = self.surface(self.factory_seed)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = 0, self.width / 2, self.width / 2, self.width / 2 + self.sleeve_length * np.sin(
+ self.sleeve_angle), self.width / 2 + self.sleeve_length * np.sin(
+ self.sleeve_angle) + self.sleeve_width * np.cos(
+ self.sleeve_angle), self.width / 2, self.width / 4, 0
+
+ y_anchors = 0, 0, self.size - self.sleeve_width / np.sin(
+ self.sleeve_angle), self.size - self.sleeve_width / np.sin(
+ self.sleeve_angle) - self.sleeve_length * np.cos(
+ self.sleeve_angle), self.size - self.sleeve_width / np.sin(
+ self.sleeve_angle) - self.sleeve_length * np.cos(self.sleeve_angle) + self.sleeve_width * np.sin(
+ self.sleeve_angle), self.size, self.size + self.size_neck, self.size + self.size_neck * uniform(.3,
+ .7)
+
+ obj = new_circle(vertices=len(x_anchors))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.edge_face_add()
+ bpy.ops.mesh.flip_normals()
+ write_co(obj, np.stack([x_anchors, y_anchors, np.zeros_like(x_anchors)], -1))
+ butil.modify_mesh(obj, 'MIRROR', use_axis=(True, False, False))
+ remesh_fill(obj, .02)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ x, y, z = read_center(obj).T
+ x_, y_, z_ = read_normal(obj).T
+ remove_faces(obj, (y_ < -.5) | ((y_ > .5) & (x_ * x < 0)))
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.remove_doubles(threshold=1e-3)
+ butil.modify_mesh(obj, 'BEVEL', width=self.sleeve_width * uniform(.1, .15))
+ subsurf(obj, 1)
+ wrap_front_back(obj, self.surface)
+ return obj
diff --git a/infinigen/assets/clothes/towel.py b/infinigen/assets/clothes/towel.py
new file mode 100644
index 000000000..964191c44
--- /dev/null
+++ b/infinigen/assets/clothes/towel.py
@@ -0,0 +1,123 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+from scipy.optimize import fsolve
+
+from infinigen.assets.elements.rug import ArtRug
+from infinigen.assets.materials import rug
+from infinigen.assets.utils.decorate import geo_extension, mirror, read_co, read_edge_direction, \
+ subdivide_edge_ring, subsurf, write_co
+from infinigen.assets.utils.object import center, new_plane
+from infinigen.assets.utils.uv import unwrap_faces, wrap_sides
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import normalize
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class TowelFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(TowelFactory, self).__init__(factory_seed, coarse)
+ self.width = log_uniform(.3, .6)
+ self.length = self.width * log_uniform(1, 1.5)
+ self.thickness = log_uniform(.003, .01)
+ prob = np.array([2, 1])
+ self.fold_type = np.random.choice(['fold', 'roll'], p=prob / prob.sum())
+ self.folds = np.random.randint(2, 4)
+ self.extra_thickness = self.thickness * uniform(.2, .3)
+ self.fold_count = 15
+ self.roll_count = 256
+ self.roll_total = self.compute_roll_total()
+ materials = AssetList['TowelFactory']()
+ self.surface = materials['surface'].assign_material()
+ if self.surface == ArtRug:
+ self.surface = self.surface(self.factory_seed)
+
+ def fold(self, obj):
+ x, y, z = read_co(obj).T
+ if np.max(x) - np.min(x) > np.max(y) - np.min(y):
+ obj.rotation_euler[-1] = np.pi * (uniform() < .5)
+ else:
+ obj.rotation_euler[-1] = np.pi * (uniform() < .5) + np.pi / 2
+ butil.apply_transform(obj, True)
+ obj.location = *(-center(obj))[:-1], 0
+ obj.location[0] += uniform(-self.thickness, self.thickness)
+ butil.apply_transform(obj, True)
+ n = len(obj.data.vertices)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.edges.ensure_lookup_table()
+ selected = np.abs(read_edge_direction(obj)[:, 0]) > 1 - 1e-3
+ edges = [bm.edges[i] for i in np.nonzero(selected)[0]]
+ bmesh.ops.subdivide_edgering(bm, edges=edges, cuts=self.fold_count, smooth=2)
+ bmesh.update_edit_mesh(obj.data)
+ co = read_co(obj)
+ order = np.where(co[n::self.fold_count, 0] < co[n + 1::self.fold_count, 0], 1, -1)
+ x_ = np.linspace(-self.thickness * order, self.thickness * order, self.fold_count).T.ravel()
+ co[n:, 0] = x_
+ x, y, z = co.T
+ max_z = np.max(z) + self.extra_thickness
+ theta = x / self.thickness * np.pi / 2
+ x__ = np.where(x < -self.thickness, x,
+ np.where(x > self.thickness, -x, -self.thickness + (max_z - z) * np.cos(theta)))
+ z_ = np.where(x < -self.thickness, z,
+ np.where(x > self.thickness, max_z * 2 - z, max_z + (max_z - z) * np.sin(theta)))
+ write_co(obj, np.stack([x__, y, z_], -1))
+ if uniform() < .5:
+ mirror(obj)
+ return obj
+
+ def compute_roll_total(self):
+ c = self.length / (self.thickness + self.extra_thickness) * (4 * np.pi)
+ f = lambda t: t * np.sqrt(1 + t * t) + np.log(t + np.sqrt(1 + t * t)) - c
+ return fsolve(f, np.zeros(1))[0]
+
+ def pre_roll(self, obj):
+ subdivide_edge_ring(obj, self.roll_count, (1, 0, 0))
+ x, y, z = read_co(obj).T
+ i = np.round((x / self.length + .5) * self.roll_count).astype(int)
+ t = np.linspace(0, self.roll_total, self.roll_count + 1)[i]
+ length = (t * np.sqrt(1 + t * t) + np.log(t + np.sqrt(1 + t * t))) * (
+ self.thickness + self.extra_thickness) / (4 * np.pi)
+ write_co(obj, np.stack([length, y, z], -1))
+ return i
+
+ def roll(self, obj, i):
+ t = np.linspace(0, self.roll_total, self.roll_count + 1)[np.concatenate([i, i])]
+ x, y, z = read_co(obj).T
+ r = (self.thickness + self.extra_thickness) / (2 * np.pi) * t + np.where(z > self.thickness / 2,
+ -self.thickness / 2,
+ self.thickness / 2)
+ write_co(obj, np.stack([r * np.cos(t), y, r * np.sin((t))], -1))
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_plane()
+ if self.fold_type == 'roll':
+ obj.scale = self.length / 2, self.width / 2, 1
+ else:
+ obj.scale = self.width / 2, self.length / 2, 1
+ butil.apply_transform(obj, True)
+ i = None
+ if self.fold_type == 'roll':
+ i = self.pre_roll(obj)
+ wrap_sides(obj, self.surface, 'z', 'x', 'y', strength=uniform(.2, .4))
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=1)
+ if self.fold_type == 'roll':
+ self.roll(obj, i)
+ subdivide_edge_ring(obj, 16, (0, 1, 0))
+ else:
+ for _ in range(self.folds):
+ self.fold(obj)
+ subdivide_edge_ring(obj, 16, (1, 0, 0))
+ subdivide_edge_ring(obj, 16, (0, 1, 0))
+ butil.modify_mesh(obj, 'BEVEL', width=self.thickness * uniform(.4, .8), segments=2)
+ surface.add_geomod(obj, geo_extension, apply=True, input_args=[uniform(.05, .1)])
+ subsurf(obj, 1)
+ return obj
diff --git a/infinigen/assets/color_fits.py b/infinigen/assets/color_fits.py
new file mode 100644
index 000000000..a04a3b40f
--- /dev/null
+++ b/infinigen/assets/color_fits.py
@@ -0,0 +1,98 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Stamatis Alexandropoulos, Meenal Parakh
+
+import os
+import numpy as np
+from matplotlib import pyplot as plt
+import time
+
+from infinigen.core.util.color import hsv2rgba
+
+manual_fits = {
+ 'sofa_fabric': {
+ 'means': np.array([[0.1, 0.25], [0.5, 0.7], [0.65, 0.15]]),
+ 'covariances': [
+ 0.7*np.array([[0.01, 0], [0, 0.04]]),
+ 0.7*np.array([[0.02, 0], [0, 0.02]]),
+ 0.7*np.array([[0.03, 0], [-0.01, 0.012]])
+ ],
+ 'probabilities': [0.5, 0.3, 0.2],
+ 'n_components': 3
+ },
+ 'sofa_leather': {
+ 'means': np.array([[0.07, 0.45], [0.6, 0.3]]),
+ 'covariances': [
+ 0.7*np.array([[0.005, 0], [0, 0.09]]),
+ 0.7*np.array([[0.015, 0], [0, 0.04]])
+ ],
+ 'min_val': [0.04, 0.04],
+ 'max_val': [0.75, 0.85],
+ 'probabilities': [0.7, 0.3],
+ 'n_components': 2
+ },
+ 'sofa_linen': {
+ 'means': np.array([[0.12, 0.5], [0.6, 0.4], [0.9, 0.2]]),
+ 'covariances': [
+ 0.7*np.array([[0.01, 0], [0, 0.12]]),
+ 0.7*np.array([[0.01, 0], [0, 0.09]]),
+ 0.7*np.array([[0.01, 0], [0, 0.02]])
+ ],
+ 'probabilities': [0.8, 0.15, 0.05],
+ 'n_components': 3
+ },
+ 'sofa_velvet': {
+ 'means': np.array([[0.52, 0.45]]),
+ 'covariances': [
+ np.array([[0.2, 0], [0, 0.2]])
+ ],
+ 'probabilities': [1.0],
+ 'n_components': 1
+ },
+ 'bedding_sheet': {
+ 'means': np.array([[0.1, 0.4], [0.6, 0.2]]),
+ 'covariances': [
+ 0.7*np.array([[0.01, 0], [0, 0.1]]),
+ 0.7*np.array([[0.03, 0], [-0.01, 0.02]])
+ ],
+ 'probabilities': [0.9, 0.1],
+ 'n_components': 2
+ }
+}
+
+val_params = {
+ 'bedding_sheet': {'min_val': 0.15, 'max_val': 0.94, 'mu': 0.66, 'std': 0.17},
+ 'sofa_fabric': {'min_val': 0.10, 'max_val': 0.88, 'mu': 0.47, 'std': 0.23},
+ 'sofa_leather': {'min_val': 0.06, 'max_val': 0.93, 'mu': 0.40, 'std': 0.2},
+ 'sofa_linen': {'min_val': 0.15, 'max_val': 0.86, 'mu': 0.55, 'std': 0.2},
+ 'sofa_velvet': {'min_val': 0.11, 'max_val': 0.70, 'mu': 0.35, 'std': 0.18},
+}
+
+
+
+def get_val(mu=0.5, std=0.2, min_val=0.1, max_val=0.9):
+ val = np.random.normal(mu, std)
+ val = np.clip(val, min_val, max_val)
+ return val
+
+
+def real_color_distribution(name):
+ params = manual_fits[name]
+
+ num_gaussians = params['n_components']
+ idx = np.random.choice(num_gaussians, p=params['probabilities'])
+
+ mu = params['means'][idx]
+ cov = params['covariances'][idx]
+
+ h, s = np.random.multivariate_normal(mu, cov)
+ min_val = params.get('min_val', 0.0)
+ max_val = params.get('max_val', 1.0)
+
+ h, s = np.clip([h, s], min_val, max_val)
+
+ v = get_val(**(val_params[name])) * 0.1
+ rgba = hsv2rgba([h, s, v])
+
+ return rgba
diff --git a/infinigen/assets/corals/diff_growth.py b/infinigen/assets/corals/diff_growth.py
index cfd799951..036f104ec 100644
--- a/infinigen/assets/corals/diff_growth.py
+++ b/infinigen/assets/corals/diff_growth.py
@@ -16,7 +16,7 @@
import infinigen.core.util.blender as butil
from infinigen.core import surface
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class DiffGrowthBaseCoralFactory(BaseCoralFactory):
default_scale = [1] * 3
diff --git a/infinigen/assets/corals/elkhorn.py b/infinigen/assets/corals/elkhorn.py
index 90485844d..57302fdf0 100644
--- a/infinigen/assets/corals/elkhorn.py
+++ b/infinigen/assets/corals/elkhorn.py
@@ -4,25 +4,24 @@
# Authors: Lingjie Mei
-import bmesh
import bpy
+import bmesh
import numpy as np
from mathutils import kdtree
from numpy.random import uniform
from infinigen.assets.corals.base import BaseCoralFactory
from infinigen.assets.corals.tentacles import make_radius_points_fn
-from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, remove_vertices, \
- separate_loose, write_co
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, remove_vertices, write_co
from infinigen.assets.utils.draw import make_circular_interp
-from infinigen.assets.utils.misc import log_uniform
-from infinigen.assets.utils.object import new_circle, origin2lowest
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.object import new_circle, origin2lowest, separate_loose
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class ElkhornBaseCoralFactory(BaseCoralFactory):
tentacle_prob = 0.
diff --git a/infinigen/assets/corals/fan.py b/infinigen/assets/corals/fan.py
index c553f773d..58fe2ca76 100644
--- a/infinigen/assets/corals/fan.py
+++ b/infinigen/assets/corals/fan.py
@@ -4,14 +4,15 @@
# Authors: Lingjie Mei
-import bmesh
import bpy
+import bmesh
import numpy as np
from numpy.random import uniform
import infinigen.core.util.blender as butil
from infinigen.assets.corals.base import BaseCoralFactory
-from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, subsurface2face_size, treeify
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension, read_co, subsurface2face_size
+from infinigen.assets.utils.mesh import treeify
from infinigen.assets.utils.draw import shape_by_angles
from infinigen.assets.utils.nodegroup import geo_radius
from infinigen.assets.utils.object import new_circle, origin2lowest
@@ -19,7 +20,7 @@
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class FanBaseCoralFactory(BaseCoralFactory):
diff --git a/infinigen/assets/corals/generate.py b/infinigen/assets/corals/generate.py
index 0b99dbab4..aa476581a 100644
--- a/infinigen/assets/corals/generate.py
+++ b/infinigen/assets/corals/generate.py
@@ -11,9 +11,11 @@
from numpy.random import uniform
import infinigen.core.util.blender as butil
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from .fan import FanBaseCoralFactory
-from ..utils.decorate import assign_material, join_objects
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects
from infinigen.core.util.math import FixedSeed
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
@@ -30,7 +32,8 @@
from .tube import TubeBaseCoralFactory
from .star import StarBaseCoralFactory
from . import tentacles
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.nodes.node_utils import build_color_ramp
class CoralFactory(AssetFactory):
@@ -63,19 +66,19 @@ def create_asset(self, face_size=0.01, realize=True, **params):
else:
self.apply_bump(obj)
+ tag_object(obj, 'coral')
+
if uniform(0, 1) < self.factory.tentacle_prob and not has_bump:
t = tentacles.apply(obj, self.factory.points_fn, self.factory.density, realize, self.base_hue)
obj = join_objects([obj, t])
- tag_object(obj, 'coral')
-
return obj
def apply_noise_texture(self, obj):
t = np.random.choice(['STUCCI', 'MARBLE'])
texture = bpy.data.textures.new(name='coral', type=t)
texture.noise_scale = log_uniform(.01, .02)
- butil.modify_mesh(obj, 'DISPLACE', True, strength=self.factory.noise_strength * uniform(.8, 1.5),
+ butil.modify_mesh(obj, 'DISPLACE', True, strength=self.factory.noise_strength * uniform(.9, 1.2),
mid_level=0, texture=texture)
def apply_bump(self, obj):
@@ -98,10 +101,10 @@ def build_base_hue():
@staticmethod
def shader_coral(nw: NodeWrangler, base_hue):
shift = uniform(.05, .1) * (-1) ** np.random.randint(2)
- subsurface_color = *colorsys.hsv_to_rgb(uniform(0, 1), uniform(0, 1), 1.), 1
- bright_color = *colorsys.hsv_to_rgb((base_hue + shift) % 1, uniform(.7, .9), .2), 1
- dark_color = *colorsys.hsv_to_rgb(base_hue, uniform(.5, .7), .1), 1
- light_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.2, .2)) % 1, uniform(.2, .4), .4), 1
+ subsurface_color = hsv2rgba(uniform(0, 1), uniform(0, 1), 1.)
+ bright_color = hsv2rgba((base_hue + shift) % 1, uniform(.7, .9), .2)
+ dark_color = hsv2rgba(base_hue, uniform(.5, .7), .1)
+ light_color = hsv2rgba((base_hue + uniform(-.2, .2)) % 1, uniform(.2, .4), .4)
specular = uniform(.25, .5)
color = build_color_ramp(nw, nw.musgrave(uniform(10, 20)), [.0, .3, .7, 1.],
diff --git a/infinigen/assets/corals/laplacian.py b/infinigen/assets/corals/laplacian.py
index 261e5cd96..dc10d769a 100644
--- a/infinigen/assets/corals/laplacian.py
+++ b/infinigen/assets/corals/laplacian.py
@@ -11,7 +11,7 @@
from infinigen.assets.utils.decorate import geo_extension
import infinigen.core.util.blender as butil
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class CauliflowerBaseCoralFactory(BaseCoralFactory):
tentacle_prob = 0.4
diff --git a/infinigen/assets/corals/reaction_diffusion.py b/infinigen/assets/corals/reaction_diffusion.py
index f966ba9bf..3acb7eb77 100644
--- a/infinigen/assets/corals/reaction_diffusion.py
+++ b/infinigen/assets/corals/reaction_diffusion.py
@@ -15,7 +15,7 @@
import infinigen.core.util.blender as butil
from infinigen.core.util.math import FixedSeed
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class ReactionDiffusionBaseCoralFactory(BaseCoralFactory):
tentacle_prob = 0.
diff --git a/infinigen/assets/corals/star.py b/infinigen/assets/corals/star.py
index 825db8d1e..9f5c791f1 100644
--- a/infinigen/assets/corals/star.py
+++ b/infinigen/assets/corals/star.py
@@ -4,21 +4,21 @@
# Authors: Lingjie Mei
-import bmesh
import bpy
+import bmesh
import numpy as np
from mathutils import Vector
from numpy.random import uniform
import infinigen.core.util.blender as butil
from infinigen.assets.corals.base import BaseCoralFactory
-from infinigen.assets.utils.decorate import displace_vertices, geo_extension, join_objects
-from infinigen.assets.utils.object import new_empty, new_icosphere
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension
+from infinigen.assets.utils.object import join_objects, new_empty, new_icosphere
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class StarBaseCoralFactory(BaseCoralFactory):
tentacle_prob = 1.
@@ -65,7 +65,7 @@ def geo_flower(nw: NodeWrangler, size, resolution, anchor):
normal = nw.new_node(Nodes.NamedAttribute, ['custom_normal'], attrs={'data_type': 'FLOAT_VECTOR'})
geometry = nw.new_node(Nodes.SetPosition, [geometry, None, None, nw.scale(offset, normal)])
outer = nw.boolean_math('AND', nw.compare('GREATER_THAN', t, .4), nw.compare('LESS_THAN', t, .6))
- geometry = nw.new_node(Nodes.StoreNamedAttribute,
+ geometry = nw.new_node(Nodes.StoreNamedAttribute,
input_kwargs={'Geometry': geometry, 'Name': 'outermost', 'Value': outer})
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
diff --git a/infinigen/assets/corals/tentacles.py b/infinigen/assets/corals/tentacles.py
index af02c3ae6..f18bb54a3 100644
--- a/infinigen/assets/corals/tentacles.py
+++ b/infinigen/assets/corals/tentacles.py
@@ -8,16 +8,16 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.misc import sample_direction
-from infinigen.assets.utils.decorate import assign_material
+from infinigen.assets.utils.misc import assign_material, sample_direction
from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.core.util.color import hsv2rgba
from infinigen.core.placement.factory import make_asset_collection
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
from infinigen.core import surface
from infinigen.assets.trees.tree import build_radius_tree
import infinigen.core.util.blender as butil
from infinigen.core.util.blender import deep_clone_obj
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup, COMBINED_ATTR_NAME
def build_tentacles(**kwargs):
n_branch = 5
@@ -29,7 +29,6 @@ def build_tentacles(**kwargs):
obj = build_radius_tree(None, branch_config, uniform(.002, .004))
surface.add_geomod(obj, geo_radius, apply=True, input_args=['radius'])
- tag_object(obj, 'tentacle')
return obj
@@ -85,20 +84,20 @@ def geo_tentacles(nw: NodeWrangler, tentacles, points_fn=None, density=500, real
realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': tentacles})
else:
realize_instances = tentacles
-
+
group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances})
def shader_tentacles(nw: NodeWrangler, base_hue=.3):
roughness = .8
specular = .25
- color = *colorsys.hsv_to_rgb((base_hue + uniform(-0.1, 0.1)) % 1, uniform(.4, .6), .5), 1
+ color = hsv2rgba((base_hue + uniform(-0.1, 0.1)) % 1, uniform(.4, .6), .5)
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
'Base Color': color,
'Roughness': roughness,
'Specular': specular,
'Subsurface': .01})
- fresnel_color = *colorsys.hsv_to_rgb(uniform(0, 1), .6, .6), 1
+ fresnel_color = hsv2rgba(uniform(0, 1), .6, .6)
fresnel_bdsf = nw.new_node(Nodes.PrincipledBSDF, [fresnel_color])
mixed_shader = nw.new_node(Nodes.MixShader, [nw.new_node(Nodes.Fresnel), principled_bsdf, fresnel_bdsf])
return mixed_shader
@@ -106,6 +105,9 @@ def shader_tentacles(nw: NodeWrangler, base_hue=.3):
def apply(obj, points_fn, density, realize=True, base_hue=.3):
tentacles = deep_clone_obj(obj)
+ if COMBINED_ATTR_NAME in tentacles.data.attributes:
+ tentacles.data.attributes.remove(tentacles.data.attributes[COMBINED_ATTR_NAME])
+
instances = make_asset_collection(build_tentacles, 5, 'spikes', verbose=False)
surface.add_geomod(tentacles, geo_tentacles, apply=realize,
input_args=[instances, points_fn, density, realize])
diff --git a/infinigen/assets/corals/tree.py b/infinigen/assets/corals/tree.py
index 0df8febc3..09a9bbbff 100644
--- a/infinigen/assets/corals/tree.py
+++ b/infinigen/assets/corals/tree.py
@@ -11,15 +11,14 @@
from infinigen.assets.corals.base import BaseCoralFactory
from infinigen.assets.corals.tentacles import make_radius_points_fn
-from infinigen.assets.utils.decorate import separate_loose
-from infinigen.assets.utils.object import mesh2obj, data2mesh
+from infinigen.assets.utils.object import mesh2obj, data2mesh, separate_loose
from infinigen.assets.utils.nodegroup import geo_radius
import infinigen.core.util.blender as butil
from infinigen.core.placement.detail import remesh_with_attrs
from infinigen.core.util.math import FixedSeed
from infinigen.core import surface
from infinigen.assets.trees.tree import build_radius_tree, recursive_path, FineTreeVertices
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class TreeBaseCoralFactory(BaseCoralFactory):
default_scale = [1] * 3
diff --git a/infinigen/assets/corals/tube.py b/infinigen/assets/corals/tube.py
index 34b2aa146..07115eee4 100644
--- a/infinigen/assets/corals/tube.py
+++ b/infinigen/assets/corals/tube.py
@@ -14,7 +14,7 @@
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class TubeBaseCoralFactory(BaseCoralFactory):
default_scale = [.7] * 3
diff --git a/infinigen/assets/creatures/beetle.py b/infinigen/assets/creatures/beetle.py
index f5a9bf5bb..0eb5a1225 100644
--- a/infinigen/assets/creatures/beetle.py
+++ b/infinigen/assets/creatures/beetle.py
@@ -24,7 +24,7 @@
from infinigen.core.util.math import lerp, clip_gaussian, FixedSeed
from infinigen.core import surface
import infinigen.assets.materials.chitin
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
logger = logging.getLogger(__name__)
diff --git a/infinigen/assets/creatures/bird.py b/infinigen/assets/creatures/bird.py
index 52377fd55..885d2b11f 100644
--- a/infinigen/assets/creatures/bird.py
+++ b/infinigen/assets/creatures/bird.py
@@ -37,7 +37,7 @@
from infinigen.assets.creatures.util import creature, hair as creature_hair, joining
from infinigen.assets.creatures.util.animation.driver_wiggle import animate_wiggle_bones
from infinigen.assets.creatures.util.animation import idle, run_cycle
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.core.placement import animation_policy
diff --git a/infinigen/assets/creatures/carnivore.py b/infinigen/assets/creatures/carnivore.py
index c5ed8e360..7b84135c1 100644
--- a/infinigen/assets/creatures/carnivore.py
+++ b/infinigen/assets/creatures/carnivore.py
@@ -3,7 +3,6 @@
# Authors: Alexander Raistrick
-
import numpy as np
from numpy.random import uniform as U, normal as N
import gin
@@ -27,7 +26,7 @@
from infinigen.assets.creatures.util import hair as creature_hair, cloth_sim
from infinigen.assets.creatures.util.animation import idle, run_cycle
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.core.util import blender as butil
@@ -209,7 +208,7 @@ def create_asset(self, i, placeholder, **kwargs):
genome = tiger_genome()
root, parts = creature.genome_to_creature(genome, name=f'carnivore({self.factory_seed}, {i})')
- tag_object(root, 'carnivore')
+ # tag_object(root, 'carnivore')
offset_center(root)
dynamic = self.animation_mode is not None
diff --git a/infinigen/assets/creatures/crustacean.py b/infinigen/assets/creatures/crustacean.py
index 9435532fd..09f9e7f1b 100644
--- a/infinigen/assets/creatures/crustacean.py
+++ b/infinigen/assets/creatures/crustacean.py
@@ -22,14 +22,17 @@
from infinigen.assets.creatures.parts.crustacean.leg import CrabLegFactory, LobsterLegFactory
from infinigen.assets.creatures.parts.crustacean.body import CrabBodyFactory, LobsterBodyFactory
from infinigen.assets.creatures.parts.crustacean.tail import CrustaceanTailFactory
-from infinigen.assets.utils.decorate import assign_material, read_material_index, write_material_index
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
+from infinigen.assets.utils.decorate import read_material_index, write_material_index
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.surface import read_attr_data, shaderfunc_to_material
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
+from infinigen.core.nodes.node_utils import build_color_ramp
n_legs = 4
n_limbs = 5
@@ -120,10 +123,10 @@ def build_base_hue():
def shader_crustacean(nw: NodeWrangler, params):
value_shift = log_uniform(2, 10)
base_hue = params['base_hue']
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.8, 1.), log_uniform(.02, .05) * value_shift), 1
- dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.8, 1.),
- log_uniform(.01, .02) * value_shift), 1
- light_color = *colorsys.hsv_to_rgb(base_hue, uniform(.0, .4), log_uniform(.2, 1.)), 1
+ bright_color = hsv2rgba(base_hue, uniform(.8, 1.), log_uniform(.02, .05) * value_shift)
+ dark_color = hsv2rgba((base_hue + uniform(-.05, .05)) % 1, uniform(.8, 1.),
+ log_uniform(.01, .02) * value_shift)
+ light_color = hsv2rgba(base_hue, uniform(.0, .4), log_uniform(.2, 1.))
specular = uniform(.6, .8)
specular_tint = uniform(0, 1)
clearcoat = uniform(.2, .8)
diff --git a/infinigen/assets/creatures/fish.py b/infinigen/assets/creatures/fish.py
index d97798bbc..de37a396f 100644
--- a/infinigen/assets/creatures/fish.py
+++ b/infinigen/assets/creatures/fish.py
@@ -33,7 +33,7 @@
from infinigen.assets.creatures.util.animation.driver_wiggle import animate_wiggle_bones
from infinigen.assets.creatures.util.creature_util import offset_center
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.assets.materials import fish_eye_shader
diff --git a/infinigen/assets/creatures/herbivore.py b/infinigen/assets/creatures/herbivore.py
index 8eac14d13..074220f3c 100644
--- a/infinigen/assets/creatures/herbivore.py
+++ b/infinigen/assets/creatures/herbivore.py
@@ -31,7 +31,7 @@
from infinigen.assets.creatures.util.animation import idle, run_cycle
from infinigen.assets.materials import bone, tongue, eyeball, nose, horn
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.core.util import blender as butil
@@ -219,7 +219,7 @@ def create_placeholder(self, **kwargs):
def create_asset(self, i, placeholder, **kwargs):
genome = herbivore_genome()
root, parts = creature.genome_to_creature(genome, name=f'herbivore({self.factory_seed}, {i})')
- tag_object(root, 'herbivore')
+ # tag_object(root, 'herbivore')
offset_center(root)
dynamic = self.animation_mode is not None
diff --git a/infinigen/assets/creatures/insects/dragonfly.py b/infinigen/assets/creatures/insects/dragonfly.py
index e5d3b2936..8afdf0e56 100644
--- a/infinigen/assets/creatures/insects/dragonfly.py
+++ b/infinigen/assets/creatures/insects/dragonfly.py
@@ -222,8 +222,16 @@ def geometry_dragonfly(nw: NodeWrangler, **kwargs):
realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_3})
+ # TODO replace this hacky postprocess transform
+ result = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': realize_instances,
+ 'Translation': (0.6, 0, 0), # position origin at ~center of dragonfly
+ 'Rotation': (0, 0, -np.pi / 2),
+ 'Scale': (kwargs['PostprocessScale'],) * 3
+ })
+
group_output = nw.new_node(Nodes.GroupOutput,
- input_kwargs={'Geometry': realize_instances})
+ input_kwargs={'Geometry': result})
@gin.configurable
@@ -236,7 +244,6 @@ def __init__(self, factory_seed, coarse=False, bvh=None, **_):
with FixedSeed(factory_seed):
self.genome = self.sample_geo_genome()
y = U(20, 60)
- self.scale = 0.015 * N(1, 0.1)
self.policy = animation_policy.AnimPolicyRandomForwardWalk(
forward_vec=(1, 0, 0), speed=U(7, 10),
step_range=(0.2, 7), yaw_dist=("uniform", -y, y), rot_vars=[0,0,0])
@@ -283,6 +290,7 @@ def sample_geo_genome():
'Eye Color': eye_color_rgba,
'V': U(0.0, 0.5),
'Ring Length': U(0.0, 0.3),
+ 'PostprocessScale': 0.015 * N(1, 0.1),
}
def create_placeholder(self, i, loc, rot):
@@ -306,12 +314,6 @@ def create_asset(self, placeholder, **params):
phenome = self.genome.copy()
surface.add_geomod(obj, geometry_dragonfly, apply=False, input_kwargs=phenome)
-
- obj = bpy.context.object
- obj.scale *= N(1, 0.1) * self.scale
-
obj.parent = placeholder
- obj.location.x += 0.6
- obj.rotation_euler.z = -np.pi / 2 # TODO: dragonfly should have been defined facing +X
return obj
\ No newline at end of file
diff --git a/infinigen/assets/creatures/jellyfish.py b/infinigen/assets/creatures/jellyfish.py
index 5630d3ae7..a9db482e5 100644
--- a/infinigen/assets/creatures/jellyfish.py
+++ b/infinigen/assets/creatures/jellyfish.py
@@ -7,7 +7,6 @@
import colorsys
-import bmesh
import numpy as np
import bpy
from mathutils import Vector
@@ -17,10 +16,13 @@
from infinigen.assets.creatures.util.animation.driver_repeated import repeated_driver
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.assets.utils.nodegroup import geo_base_selection
-from infinigen.assets.utils.object import data2mesh, mesh2obj, new_circle, new_empty, new_icosphere, origin2highest
-from infinigen.assets.utils.decorate import assign_material, geo_extension, join_objects, read_co, remove_vertices, \
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj, new_circle, new_empty, \
+ new_icosphere, origin2highest
+from infinigen.assets.utils.decorate import geo_extension, read_co, remove_vertices, \
subsurface2face_size, write_attribute, write_co
-from infinigen.assets.utils.misc import log_uniform, make_circular, make_circular_angle
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.factory import AssetFactory
@@ -30,7 +32,7 @@
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class JellyfishFactory(AssetFactory):
@@ -284,10 +286,10 @@ def build_arm(self, radius, length, bend_angle):
def shader_jellyfish(nw: NodeWrangler, base_hue, saturation, transparency):
layerweight = nw.build_float_curve(nw.new_node(Nodes.LayerWeight, input_kwargs={'Blend': 0.3}),
[(0, 0), (.4, 0), (uniform(.6, .9), 1), (1, 1)])
- emission_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .6), 1), 1
- transparent_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.1, .1)) % 1, saturation, 1), 1
+ emission_color = hsv2rgba(base_hue, uniform(.4, .6), 1)
+ transparent_color = hsv2rgba((base_hue + uniform(-.1, .1)) % 1, saturation, 1)
emission = nw.new_node(Nodes.Emission, [emission_color])
- glossy = nw.new_node(Nodes.GlossyBSDF,
+ glossy = nw.new_node(Nodes.GlossyBSDF,
input_kwargs={'Color': transparent_color, 'Roughness': uniform(0.8, 1)})
transparent = nw.new_node(Nodes.TransparentBSDF, [transparent_color])
mix_shader = nw.new_node(Nodes.MixShader, [0.5, glossy, transparent])
diff --git a/infinigen/assets/creatures/parts/beak.py b/infinigen/assets/creatures/parts/beak.py
index da77a65da..31a29044c 100644
--- a/infinigen/assets/creatures/parts/beak.py
+++ b/infinigen/assets/creatures/parts/beak.py
@@ -14,7 +14,7 @@
from infinigen.core.util import blender as butil
from infinigen.assets.creatures.util.geometry import nurbs as nurbs_util
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def square(x):
return x * x
diff --git a/infinigen/assets/creatures/parts/body.py b/infinigen/assets/creatures/parts/body.py
index c8cd5d039..90b72ec1a 100644
--- a/infinigen/assets/creatures/parts/body.py
+++ b/infinigen/assets/creatures/parts/body.py
@@ -21,7 +21,7 @@
from infinigen.assets.creatures.util import part_util
from infinigen.assets.creatures.util.geometry import lofting, nurbs
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_quadruped_body', singleton=False, type='GeometryNodeTree')
def nodegroup_quadruped_body(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/crustacean/antenna.py b/infinigen/assets/creatures/parts/crustacean/antenna.py
index 6e0efce51..c63a0b544 100644
--- a/infinigen/assets/creatures/parts/crustacean/antenna.py
+++ b/infinigen/assets/creatures/parts/crustacean/antenna.py
@@ -11,8 +11,9 @@
from infinigen.assets.creatures.util.creature import Part
from infinigen.assets.creatures.util.genome import Joint
from infinigen.assets.creatures.parts.crustacean.leg import CrabLegFactory
-from infinigen.assets.utils.decorate import displace_vertices, join_objects
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.assets.utils.decorate import displace_vertices
+from infinigen.assets.utils.object import join_objects
+from infinigen.core.util.random import log_uniform
class LobsterAntennaFactory(CrabLegFactory):
diff --git a/infinigen/assets/creatures/parts/crustacean/body.py b/infinigen/assets/creatures/parts/crustacean/body.py
index 93fda284c..56be383ab 100644
--- a/infinigen/assets/creatures/parts/crustacean/body.py
+++ b/infinigen/assets/creatures/parts/crustacean/body.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
@@ -11,15 +12,15 @@
from infinigen.assets.creatures.util.creature import Part, PartFactory
from infinigen.assets.creatures.util.genome import Joint
from infinigen.assets.creatures.parts.utils.draw import geo_symmetric_texture
-from infinigen.assets.utils.decorate import add_distance_to_boundary, displace_vertices, join_objects, read_co, write_co
+from infinigen.assets.utils.decorate import distance2boundary, displace_vertices, read_co, write_co
from infinigen.assets.utils.draw import leaf, spin
from infinigen.assets.utils.misc import log_uniform
-from infinigen.assets.utils.object import new_line
+from infinigen.assets.utils.object import join_objects, new_line
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.placement import placeholder_locs
from infinigen.core import surface
-from infinigen.core.surface import write_attr_data
+from infinigen.core.surface import read_attr_data, write_attr_data
from infinigen.core.util import blender as butil
@@ -103,12 +104,11 @@ def make_surface(params):
vector_locations = []
obj = leaf(x_anchors, y_anchors, vector_locations)
butil.modify_mesh(obj, 'SUBSURF', levels=1, render_levels=1)
- add_distance_to_boundary(obj)
+ distance2boundary(obj)
return obj
def make_surface_side(self, obj, params, prefix="upper"):
- vg = obj.vertex_groups['distance']
- distance = np.array([vg.weight(i) for i in range(len(obj.data.vertices))])
+ distance = read_attr_data(obj, 'distance')
height_scale = interp1d([0, .5, 1], [0, params[f'{prefix}_alpha'], 1], 'quadratic')
displace_vertices(obj, lambda x, y, z: (
0, 0, (1 if prefix == 'upper' else -1) * height_scale(distance) * params[f'{prefix}_z']))
diff --git a/infinigen/assets/creatures/parts/crustacean/claw.py b/infinigen/assets/creatures/parts/crustacean/claw.py
index f042ce85a..9773e69d4 100644
--- a/infinigen/assets/creatures/parts/crustacean/claw.py
+++ b/infinigen/assets/creatures/parts/crustacean/claw.py
@@ -13,9 +13,10 @@
from infinigen.assets.creatures.util.genome import Joint
from infinigen.assets.creatures.parts.crustacean.leg import CrabLegFactory
from infinigen.assets.creatures.parts.utils.draw import decorate_segment
-from infinigen.assets.utils.decorate import displace_vertices, join_objects, read_co, remove_vertices
+from infinigen.assets.utils.decorate import displace_vertices, read_co, remove_vertices
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import spin
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.assets.utils.nodegroup import geo_base_selection
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
diff --git a/infinigen/assets/creatures/parts/crustacean/eye.py b/infinigen/assets/creatures/parts/crustacean/eye.py
index 87f048a53..c07eac775 100644
--- a/infinigen/assets/creatures/parts/crustacean/eye.py
+++ b/infinigen/assets/creatures/parts/crustacean/eye.py
@@ -9,8 +9,7 @@
from numpy.random import uniform
from infinigen.assets.creatures.util.creature import Part, PartFactory
-from infinigen.assets.utils.decorate import join_objects
-from infinigen.assets.utils.object import new_icosphere, origin2leftmost
+from infinigen.assets.utils.object import join_objects, new_icosphere, origin2leftmost
from infinigen.core.placement.detail import remesh_with_attrs
diff --git a/infinigen/assets/creatures/parts/crustacean/leg.py b/infinigen/assets/creatures/parts/crustacean/leg.py
index 82136f3d1..b16e359c7 100644
--- a/infinigen/assets/creatures/parts/crustacean/leg.py
+++ b/infinigen/assets/creatures/parts/crustacean/leg.py
@@ -11,8 +11,9 @@
from infinigen.assets.creatures.util.creature import Part, PartFactory
from infinigen.assets.creatures.util.genome import Joint
from infinigen.assets.creatures.parts.utils.draw import make_segments
-from infinigen.assets.utils.decorate import join_objects, read_co
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.assets.utils.decorate import read_co
+from infinigen.assets.utils.object import join_objects
+from infinigen.core.util.random import log_uniform
from infinigen.core.surface import write_attr_data
diff --git a/infinigen/assets/creatures/parts/crustacean/tail.py b/infinigen/assets/creatures/parts/crustacean/tail.py
index 99fc2971c..b961cae23 100644
--- a/infinigen/assets/creatures/parts/crustacean/tail.py
+++ b/infinigen/assets/creatures/parts/crustacean/tail.py
@@ -12,8 +12,9 @@
from infinigen.assets.creatures.util.creature import Part, PartFactory
from infinigen.assets.creatures.util.genome import Joint
from infinigen.assets.creatures.parts.utils.draw import make_segments
-from infinigen.assets.utils.decorate import join_objects, read_co
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.assets.utils.decorate import read_co
+from infinigen.assets.utils.object import join_objects
+from infinigen.core.util.random import log_uniform
from infinigen.core.surface import write_attr_data
diff --git a/infinigen/assets/creatures/parts/eye.py b/infinigen/assets/creatures/parts/eye.py
index 741e60c89..e3ee25d3f 100644
--- a/infinigen/assets/creatures/parts/eye.py
+++ b/infinigen/assets/creatures/parts/eye.py
@@ -23,7 +23,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util import part_util
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_eyelid', singleton=True, type='GeometryNodeTree')
def nodegroup_eyelid(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/fin_old.py b/infinigen/assets/creatures/parts/fin_old.py
index 28913c1a1..510e5bf77 100644
--- a/infinigen/assets/creatures/parts/fin_old.py
+++ b/infinigen/assets/creatures/parts/fin_old.py
@@ -19,7 +19,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_fish_fin', singleton=False, type='GeometryNodeTree')
def nodegroup_fish_fin(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/foot.py b/infinigen/assets/creatures/parts/foot.py
index e5df80357..724f6159b 100644
--- a/infinigen/assets/creatures/parts/foot.py
+++ b/infinigen/assets/creatures/parts/foot.py
@@ -19,7 +19,7 @@
from infinigen.assets.creatures.util.creature import Part, PartFactory
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_tiger_toe', singleton=False, type='GeometryNodeTree')
def nodegroup_tiger_toe(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/generic_nurbs.py b/infinigen/assets/creatures/parts/generic_nurbs.py
index c5f3f8a92..9e92d917d 100644
--- a/infinigen/assets/creatures/parts/generic_nurbs.py
+++ b/infinigen/assets/creatures/parts/generic_nurbs.py
@@ -20,7 +20,7 @@
from infinigen.core.util import blender as butil
from infinigen.core.util.logging import Suppress
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
NURBS_BASE_PATH = Path(__file__).parent/'nurbs_data'
NURBS_KEYS = [p.stem for p in NURBS_BASE_PATH.iterdir()]
diff --git a/infinigen/assets/creatures/parts/head.py b/infinigen/assets/creatures/parts/head.py
index 341ed7078..caf2b7af6 100644
--- a/infinigen/assets/creatures/parts/head.py
+++ b/infinigen/assets/creatures/parts/head.py
@@ -23,7 +23,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util import part_util
from infinigen.assets.creatures.parts.eye import nodegroup_mammal_eye
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_carnivore_jaw', singleton=True, type='GeometryNodeTree')
def nodegroup_carnivore_jaw(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/head_detail.py b/infinigen/assets/creatures/parts/head_detail.py
index b342d7d6d..f1fb163fe 100644
--- a/infinigen/assets/creatures/parts/head_detail.py
+++ b/infinigen/assets/creatures/parts/head_detail.py
@@ -21,7 +21,7 @@
from infinigen.assets.creatures.util.nodegroups.geometry import nodegroup_solidify, nodegroup_symmetric_clone, nodegroup_taper
from infinigen.core.util.math import clip_gaussian
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_cat_ear', singleton=False, type='GeometryNodeTree')
def nodegroup_cat_ear(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/hoof.py b/infinigen/assets/creatures/parts/hoof.py
index 16b379883..9662c085c 100644
--- a/infinigen/assets/creatures/parts/hoof.py
+++ b/infinigen/assets/creatures/parts/hoof.py
@@ -25,7 +25,7 @@
from infinigen.core.nodes import node_utils
from infinigen.assets.creatures.util.geometry import nurbs as nurbs_util
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def square(x):
return x * x
diff --git a/infinigen/assets/creatures/parts/horn.py b/infinigen/assets/creatures/parts/horn.py
index 615c8d0c9..271d76e6b 100644
--- a/infinigen/assets/creatures/parts/horn.py
+++ b/infinigen/assets/creatures/parts/horn.py
@@ -21,7 +21,7 @@
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
from infinigen.core.nodes import node_utils
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_noise', singleton=False, type='GeometryNodeTree')
def nodegroup_noise(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/leg.py b/infinigen/assets/creatures/parts/leg.py
index 2cf9a5d4a..afe40be65 100644
--- a/infinigen/assets/creatures/parts/leg.py
+++ b/infinigen/assets/creatures/parts/leg.py
@@ -21,7 +21,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_quadruped_back_leg', singleton=False, type='GeometryNodeTree')
def nodegroup_quadruped_back_leg(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/ridged_fin.py b/infinigen/assets/creatures/parts/ridged_fin.py
index df2ee929d..6c8cb5171 100644
--- a/infinigen/assets/creatures/parts/ridged_fin.py
+++ b/infinigen/assets/creatures/parts/ridged_fin.py
@@ -22,7 +22,7 @@
from infinigen.assets.creatures.util.creature import PartFactory, Part
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_mix2_values', singleton=True, type='GeometryNodeTree')
def nodegroup_mix2_values(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/parts/tail.py b/infinigen/assets/creatures/parts/tail.py
index 78c088a4f..26e6d0c82 100644
--- a/infinigen/assets/creatures/parts/tail.py
+++ b/infinigen/assets/creatures/parts/tail.py
@@ -16,7 +16,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_tail', singleton=False, type='GeometryNodeTree')
def nodegroup_tail(nw: NodeWrangler):
@@ -39,7 +39,7 @@ class Tail(PartFactory):
def sample_params(self):
return {
- 'length_rad1_rad2': (N(1.5, 0.5), 0.05, 0.02),
+ 'length_rad1_rad2': (N(0.5, 0.1), 0.05, 0.02),
'angles_deg': np.array((31.39, 65.81, -106.93)) * N(1, 0.1),
'aspect': N(1, 0.05)
}
diff --git a/infinigen/assets/creatures/parts/wings.py b/infinigen/assets/creatures/parts/wings.py
index 779048f8d..4331db191 100644
--- a/infinigen/assets/creatures/parts/wings.py
+++ b/infinigen/assets/creatures/parts/wings.py
@@ -24,7 +24,7 @@
from infinigen.assets.creatures.util.creature import PartFactory
from infinigen.assets.creatures.util.part_util import nodegroup_to_part
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_feather', singleton=False, type='GeometryNodeTree')
def nodegroup_feather(nw: NodeWrangler):
diff --git a/infinigen/assets/creatures/util/cloth_sim.py b/infinigen/assets/creatures/util/cloth_sim.py
index b7151d614..c68915a90 100644
--- a/infinigen/assets/creatures/util/cloth_sim.py
+++ b/infinigen/assets/creatures/util/cloth_sim.py
@@ -28,46 +28,46 @@ def local_pos_rigity_mask(nw: NodeWrangler):
expose_input=[('NodeSocketGeometry', 'Geometry', None),
('NodeSocketFloat', 'To Min', 0.4),
('NodeSocketFloat', 'To Max', 0.9)])
-
+
separate_xyz = nw.new_node(Nodes.SeparateXYZ,
input_kwargs={'Vector': nw.expose_input('Local Pos', attribute='local_pos')})
-
+
clamp = nw.new_node(Nodes.Clamp,
input_kwargs={'Value': nw.expose_input("Radius", attribute='skeleton_rad'), 'Min': 0.03, 'Max': 0.49})
-
+
multiply = nw.new_node(Nodes.Math,
input_kwargs={0: clamp, 1: -1.0},
attrs={'operation': 'MULTIPLY'})
-
+
multiply_1 = nw.new_node(Nodes.Math,
input_kwargs={0: clamp, 1: 1.5},
attrs={'operation': 'MULTIPLY'})
-
+
map_range = nw.new_node(Nodes.MapRange,
input_kwargs={'Value': separate_xyz.outputs["Z"], 1: multiply, 2: multiply_1})
-
+
musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
input_kwargs={'W': uniform(1e3), 'Scale': normal(10, 1)},
attrs={'musgrave_dimensions': '4D'})
-
+
multiply_2 = nw.new_node(Nodes.Math,
input_kwargs={0: musgrave_texture, 1: normal(0.07, 0.007)},
attrs={'operation': 'MULTIPLY'})
-
+
musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture,
input_kwargs={'Scale': normal(5, 0.5), 'W': uniform(1e3)},
attrs={'musgrave_dimensions': '4D'})
-
+
multiply_3 = nw.new_node(Nodes.Math,
input_kwargs={0: musgrave_texture_1, 1: normal(0.12, 0.01)},
attrs={'operation': 'MULTIPLY'})
-
+
add = nw.new_node(Nodes.Math,
input_kwargs={0: multiply_2, 1: multiply_3})
-
+
add_1 = nw.new_node(Nodes.Math,
input_kwargs={0: map_range.outputs["Result"], 1: add})
-
+
colorramp = nw.new_node(Nodes.ColorRamp,
input_kwargs={'Fac': add_1})
colorramp.color_ramp.elements.new(1)
@@ -77,29 +77,35 @@ def local_pos_rigity_mask(nw: NodeWrangler):
colorramp.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
colorramp.color_ramp.elements[2].position = 1.0
colorramp.color_ramp.elements[2].color = (0.0, 0.0, 0.0, 1.0)
-
+
map_range_1 = nw.new_node(Nodes.MapRange,
input_kwargs={'Value': colorramp.outputs["Color"], 3: group_input.outputs["To Min"], 4: group_input.outputs["To Max"]})
-
+
musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture)
-
+
multiply_4 = nw.new_node(Nodes.Math,
input_kwargs={0: musgrave_texture_2, 1: normal(0.1, 0.02)},
attrs={'operation': 'MULTIPLY'})
-
+
return nw.new_node(Nodes.Math,
input_kwargs={0: map_range_1.outputs["Result"], 1: multiply_4})
-def bake_cloth(obj, settings, attributes, frame_start=None, frame_end=None):
+def bake_cloth(obj, settings=None, attributes=None, frame_start=None, frame_end=None):
if frame_start is None:
frame_start = bpy.context.scene.frame_start
if frame_end is None:
frame_end = bpy.context.scene.frame_end
+ if settings is None:
+ settings = {}
+ if attributes is None:
+ attributes = {}
mod = obj.modifiers.new('bake_cloth', 'CLOTH')
mod.settings.effector_weights.gravity = settings.pop('gravity', 1)
+ mod.collision_settings.distance_min = settings.pop('distance_min', .015)
+ mod.collision_settings.use_self_collision = settings.pop('use_self_collision', False)
for k, v in settings.items():
setattr(mod.settings, k, v)
@@ -111,7 +117,6 @@ def bake_cloth(obj, settings, attributes, frame_start=None, frame_end=None):
mod.point_cache.frame_start = frame_start
mod.point_cache.frame_end = frame_end
-
with butil.ViewportMode(obj, mode='OBJECT'), butil.SelectObjects(obj), Timer('Baking fish cloth'):
override = {'scene': bpy.context.scene, 'active_object': obj, 'point_cache': mod.point_cache}
bpy.ops.ptcache.bake(override, bake=True)
diff --git a/infinigen/assets/creatures/util/geometry/curve.py b/infinigen/assets/creatures/util/geometry/curve.py
index 8c2f8fa2f..893086a74 100644
--- a/infinigen/assets/creatures/util/geometry/curve.py
+++ b/infinigen/assets/creatures/util/geometry/curve.py
@@ -11,9 +11,9 @@
class Curve:
def __init__(
- self, points,
- profile=None, taper=None,
- closed=False, sharp=None,
+ self, points,
+ profile=None, taper=None,
+ closed=False, sharp=None,
scale=None
):
self.points = points
@@ -23,7 +23,7 @@ def __init__(
self.sharp = sharp
self.scale = scale
- def to_curve_obj(self, name='curve',
+ def to_curve_obj(self, name='curve',
resu=4, curvetype='NURBS', extrude=0, fill_caps = True,
to_mesh=False, cleanup=True
):
@@ -33,7 +33,7 @@ def to_curve_obj(self, name='curve',
curveData.resolution_u = resu
curveData.use_fill_caps = fill_caps
curveData.twist_mode = 'MINIMUM'
- curveData.extrude = extrude
+ curveData.bevel_depth = extrude
polyline = curveData.splines.new(curvetype)
@@ -89,7 +89,7 @@ def get_pos(p):
if o is not None:
o.select_set(True)
bpy.ops.object.delete(use_global=False, confirm=False)
-
+
self.profile = None
self.taper = None
diff --git a/infinigen/assets/creatures/util/geonode_part.py b/infinigen/assets/creatures/util/geonode_part.py
index 9c420aa79..ad7b91e4b 100644
--- a/infinigen/assets/creatures/util/geonode_part.py
+++ b/infinigen/assets/creatures/util/geonode_part.py
@@ -3,48 +3,13 @@
# Authors: Alexander Raistrick
-
-import pdb
-import bpy
-
import numpy as np
from infinigen.assets.creatures.util.creature import Part, Joint, infer_skeleton_from_mesh
from infinigen.core.util import blender as butil
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes, geometry_node_group_empty_new
-
-def extract_nodegroup_geo(target_obj, nodegroup, k, ng_params=None):
-
- assert k in nodegroup.outputs
- assert target_obj.type == 'MESH'
-
- vert = butil.spawn_vert('extract_nodegroup_geo.temp')
-
- butil.modify_mesh(vert, type='NODES', apply=False)
- if vert.modifiers[0].node_group == None:
- group = geometry_node_group_empty_new()
- vert.modifiers[0].node_group = group
- ng = vert.modifiers[0].node_group
- nw = NodeWrangler(ng)
- obj_inp = nw.new_node(Nodes.ObjectInfo, [target_obj])
-
- group_input_kwargs = {**ng_params}
- if 'Geometry' in nodegroup.inputs:
- group_input_kwargs['Geometry'] = obj_inp.outputs['Geometry']
- group = nw.new_node(nodegroup.name, input_kwargs=group_input_kwargs)
-
- geo = group.outputs[k]
-
- if k.endswith('Curve'):
- # curves dont export from geonodes well, convert it to a mesh
- geo = nw.new_node(Nodes.CurveToMesh, [geo])
-
- output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geo})
-
- butil.apply_modifiers(vert)
- bpy.data.node_groups.remove(ng)
- return vert
+from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo
class GeonodePartFactory:
@@ -69,7 +34,12 @@ def _extract_geo_results(self):
with butil.TemporaryObject(self.base_obj()) as base_obj:
ng = self.nodegroup_func()
geo_outputs = [o for o in ng.outputs if o.bl_socket_idname == 'NodeSocketGeometry']
- results = {o.name: extract_nodegroup_geo(base_obj, ng, o.name, ng_params=ng_params) for o in geo_outputs}
+ results = {
+ o.name: extract_nodegroup_geo(
+ base_obj, ng, o.name, ng_params=ng_params
+ )
+ for o in geo_outputs
+ }
return results
diff --git a/infinigen/assets/creatures/util/part_util.py b/infinigen/assets/creatures/util/part_util.py
index 6192e4b85..e42805929 100644
--- a/infinigen/assets/creatures/util/part_util.py
+++ b/infinigen/assets/creatures/util/part_util.py
@@ -17,39 +17,7 @@
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
-def extract_nodegroup_geo(target_obj, nodegroup, k, ng_params=None):
-
- assert k in nodegroup.outputs
- assert target_obj.type == 'MESH'
-
- vert = butil.spawn_vert('extract_nodegroup_geo.temp')
-
- butil.modify_mesh(vert, type='NODES', apply=False)
- mod = vert.modifiers[0]
- mod.node_group = bpy.data.node_groups.new('extract_nodegroup_geo', 'GeometryNodeTree')
- nw = NodeWrangler(mod.node_group)
- obj_inp = nw.new_node(Nodes.ObjectInfo, [target_obj])
-
- group_input_kwargs = {**ng_params}
- if 'Geometry' in nodegroup.inputs:
- group_input_kwargs['Geometry'] = obj_inp.outputs['Geometry']
-
- try:
- group = nw.new_node(nodegroup.name, input_kwargs=group_input_kwargs)
- except KeyError as e:
- print(f"Error while performing extract_nodegroup_geo for {nodegroup=} on {target_obj=}, output_key={k}")
- raise e
-
- geo = group.outputs[k]
-
- if k.endswith('Curve'):
- # curves dont export from geonodes well, convert it to a mesh
- geo = nw.new_node(Nodes.CurveToMesh, [geo])
-
- output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geo})
-
- butil.apply_modifiers(vert)
- return vert
+from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo
def nodegroup_to_part(nodegroup_func, params, kwargs=None, base_obj=None, split_extras=False):
diff --git a/infinigen/assets/debris/lichen.py b/infinigen/assets/debris/lichen.py
index 890a05df0..0e190a598 100644
--- a/infinigen/assets/debris/lichen.py
+++ b/infinigen/assets/debris/lichen.py
@@ -11,15 +11,18 @@
import numpy as np
from numpy.random import uniform, normal as N
-from infinigen.assets.utils.decorate import assign_material
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.color import hsv2rgba
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.placement.factory import AssetFactory, make_asset_collection
+from infinigen.core.placement.instance_scatter import scatter_instances
from infinigen.core import surface
from infinigen.core.placement.factory import AssetFactory
from infinigen.infinigen_gpl.extras.diff_growth import build_diff_growth
from infinigen.assets.utils.object import data2mesh
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class LichenFactory(AssetFactory):
@@ -46,7 +49,7 @@ def shader_lichen(nw: NodeWrangler, base_hue=.2, **params):
v_perturb = uniform(1., 1.5)
def map_perturb(h, s, v):
- return *colorsys.hsv_to_rgb(h + h_perturb, s + s_perturb, v / v_perturb), 1.
+ return hsv2rgba(h + h_perturb, s + s_perturb, v / v_perturb)
subsurface_ratio = .02
roughness = 1.
@@ -93,6 +96,6 @@ def create_asset(self, **kwargs):
butil.apply_transform(obj)
assign_material(obj, surface.shaderfunc_to_material(LichenFactory.shader_lichen,
(self.base_hue + uniform(-.04, .04)) % 1))
-
+
tag_object(obj, 'lichen')
- return obj
\ No newline at end of file
+ return obj
diff --git a/infinigen/assets/debris/moss.py b/infinigen/assets/debris/moss.py
index 63df60737..b0ab11542 100644
--- a/infinigen/assets/debris/moss.py
+++ b/infinigen/assets/debris/moss.py
@@ -1,17 +1,26 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Lingjie Mei
import math
import colorsys
from numpy.random import uniform as U
+from infinigen.core.placement.instance_scatter import scatter_instances
from infinigen.assets.utils.object import new_cube
-from infinigen.assets.utils.misc import build_color_ramp
-from infinigen.assets.utils.decorate import assign_material
-from infinigen.assets.utils.tag import tag_object
-
-from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.color import hsv2rgba
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.placement.factory import AssetFactory, make_asset_collection
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
from infinigen.core import surface
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.placement.instance_scatter import scatter_instances
+
+from infinigen.core.nodes.node_utils import build_color_ramp
class MossFactory(AssetFactory):
@@ -27,14 +36,14 @@ def shader_moss(nw: NodeWrangler, base_hue=.3):
v_perturb = U(1., 1.5)
def map_perturb(h, s, v):
- return *colorsys.hsv_to_rgb(h + h_perturb, s + s_perturb, v / v_perturb), 1.
+ return hsv2rgba(h + h_perturb, s + s_perturb, v / v_perturb)
subsurface_ratio = .05
roughness = 1.
mix_ratio = .2
- cr = build_color_ramp(nw,
- nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': 5.}).outputs["Fac"],
+ cr = build_color_ramp(nw,
+ nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': 5.}).outputs["Fac"],
[0, .5, 1],
[map_perturb(base_hue, .8, .1), map_perturb(base_hue - 0.05, .8, .1), (0., 0., 0., 1.)]
)
@@ -82,4 +91,4 @@ def geo_moss_instance(nw: NodeWrangler, face_size):
circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 4, 'Radius': radius}).outputs[
"Curve"]
mesh = nw.curve2mesh(bezier, circle)
- nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': mesh})
\ No newline at end of file
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': mesh})
diff --git a/infinigen/assets/debris/pine_needle.py b/infinigen/assets/debris/pine_needle.py
index b030e7eeb..57049e887 100644
--- a/infinigen/assets/debris/pine_needle.py
+++ b/infinigen/assets/debris/pine_needle.py
@@ -13,7 +13,7 @@
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object
+from infinigen.core.tagging import tag_object
def shader_material(nw: NodeWrangler):
# Code generated using version 2.6.3 of the node_transpiler
diff --git a/infinigen/assets/decor/__init__.py b/infinigen/assets/decor/__init__.py
new file mode 100644
index 000000000..c24bee2f6
--- /dev/null
+++ b/infinigen/assets/decor/__init__.py
@@ -0,0 +1 @@
+from .aquarium_tank import AquariumTankFactory
\ No newline at end of file
diff --git a/infinigen/assets/decor/aquarium_tank.py b/infinigen/assets/decor/aquarium_tank.py
new file mode 100644
index 000000000..627c5db35
--- /dev/null
+++ b/infinigen/assets/decor/aquarium_tank.py
@@ -0,0 +1,114 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.rocks.boulder import BoulderFactory
+from infinigen.assets.cactus import CactusFactory
+from infinigen.assets.corals import CoralFactory
+from infinigen.assets.mollusk import MolluskFactory
+from infinigen.assets.mushroom import MushroomFactory
+from infinigen.assets.underwater.seaweed import SeaweedFactory
+from infinigen.assets.materials import metal, water
+from infinigen.assets.materials import glass
+from infinigen.assets.utils.decorate import read_co, write_attribute
+from infinigen.assets.utils.object import join_objects, new_bbox, new_cube, new_plane
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class AquariumTankFactory(AssetFactory):
+ dry_factories = [MushroomFactory, CactusFactory, BoulderFactory]
+ wet_factories = [MolluskFactory, CoralFactory, SeaweedFactory]
+
+ def __init__(self, factory_seed, coarse=False):
+ super(AquariumTankFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.is_wet = uniform() < .5
+ base_factory_fn = np.random.choice(self.wet_factories if self.is_wet else self.dry_factories)
+ self.base_factory = base_factory_fn(self.factory_seed)
+ self.width = log_uniform(.5, 1)
+ self.depth = log_uniform(.5, .8)
+ self.height = log_uniform(.5, 1)
+ self.thickness = uniform(.01, .02)
+ self.belt_thickness = log_uniform(.02, .05)
+
+ materials = AssetList['AquariumTankFactory']()
+ self.glass_surface = materials['glass_surface'].assign_material()
+ self.belt_surface = materials['belt_surface'].assign_material()
+ self.water_surface = materials['water_surface'].assign_material()
+
+ scratch_prob, edge_wear_prob = materials['wear_tear_prob']
+ self.scratch, self.edge_wear = materials['wear_tear']
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ self.scratch = None
+ if not is_edge_wear:
+ self.edge_wear = None
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(
+ -self.thickness - self.depth, self.thickness, -self.thickness, self.width + self.thickness, 0, self.height
+ )
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ tank = new_cube(location=(1, 1, 1))
+ butil.apply_transform(tank, loc=True)
+ tank.scale = self.width / 2, self.depth / 2, self.height / 2
+ butil.apply_transform(tank)
+ butil.modify_mesh(tank, 'SOLIDIFY', thickness=self.thickness)
+ write_attribute(tank, 1, 'glass', 'FACE')
+ parts = [tank]
+ parts.extend(self.make_belts())
+ base_obj = self.base_factory.create_asset(**params)
+ co = read_co(base_obj)
+ x_min, x_max = np.amin(co, 0), np.amax(co, 0)
+ scale = uniform(.7, .9) / np.max((x_max - x_min) / np.array([self.width, self.depth, self.height]))
+ base_obj.location = -(x_min + x_max) * np.array(base_obj.scale) / 2
+ base_obj.location[-1] = -(x_min * base_obj.scale)[-1]
+ butil.apply_transform(base_obj, True)
+ base_obj.location = self.width / 2, self.depth / 2, self.thickness
+ base_obj.scale = [scale] * 3
+ butil.apply_transform(base_obj)
+ parts.append(base_obj)
+ obj = join_objects(parts)
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def make_belts(self):
+ belt = new_plane()
+ with butil.ViewportMode(belt, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.delete(type='ONLY_FACE')
+ belt.location = self.width / 2, self.depth / 2, 0
+ belt.scale = self.width / 2, self.depth / 2, 0
+ butil.apply_transform(belt, loc=True)
+ with butil.ViewportMode(belt, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, self.belt_thickness)})
+ butil.modify_mesh(belt, 'SOLIDIFY', thickness=self.thickness)
+ write_attribute(belt, 1, 'belt', 'FACE')
+
+ belt_ = deep_clone_obj(belt)
+ belt_.location[-1] = self.height - self.belt_thickness
+ butil.apply_transform(belt_, True)
+ return [belt, belt_]
+
+ def finalize_assets(self, assets):
+ self.glass_surface.apply(assets, selection='glass')
+ self.belt_surface.apply(assets, selection='belt')
+
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/deformed_trees/base.py b/infinigen/assets/deformed_trees/base.py
index cd56066f6..f0c610659 100644
--- a/infinigen/assets/deformed_trees/base.py
+++ b/infinigen/assets/deformed_trees/base.py
@@ -11,7 +11,8 @@
from infinigen.assets.trees import TreeFactory
from infinigen.assets.trees.generate import GenericTreeFactory, random_species
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.factory import AssetFactory
@@ -49,7 +50,7 @@ def shader_rings(nw: NodeWrangler, base_hue):
ratio = nw.new_node(Nodes.WaveTexture, [position],
input_kwargs={'Scale': uniform(10, 20), 'Distortion': uniform(4, 10)},
attrs={'wave_type': 'RINGS', 'rings_direction': 'Z', 'wave_profile': 'SAW'})
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .8), log_uniform(.2, .8)), 1.
+ bright_color = hsv2rgba(base_hue, uniform(.4, .8), log_uniform(.2, .8))
dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.02, .02)) % 1, uniform(.4, .8),
log_uniform(.02, .05)), 1.
color = nw.new_node(Nodes.MixRGB, [ratio, dark_color, bright_color])
diff --git a/infinigen/assets/deformed_trees/fallen.py b/infinigen/assets/deformed_trees/fallen.py
index a522010bd..c84970d81 100644
--- a/infinigen/assets/deformed_trees/fallen.py
+++ b/infinigen/assets/deformed_trees/fallen.py
@@ -5,20 +5,23 @@
# Authors: Lingjie Mei
-import bmesh
+
import bpy
+import bmesh
import numpy as np
from numpy.random import uniform
from infinigen.assets.deformed_trees.base import BaseDeformedTreeFactory
-from infinigen.assets.utils.decorate import assign_material, join_objects, remove_vertices, separate_loose
+from infinigen.assets.utils.decorate import remove_vertices
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects, separate_loose
from infinigen.assets.utils.draw import cut_plane
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class FallenTreeFactory(BaseDeformedTreeFactory):
diff --git a/infinigen/assets/deformed_trees/generate.py b/infinigen/assets/deformed_trees/generate.py
index 9e0313856..24ac17a63 100644
--- a/infinigen/assets/deformed_trees/generate.py
+++ b/infinigen/assets/deformed_trees/generate.py
@@ -11,7 +11,7 @@
from infinigen.assets.deformed_trees.truncated import TruncatedTreeFactory
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class DeformedTreeFactory(AssetFactory):
diff --git a/infinigen/assets/deformed_trees/hollow.py b/infinigen/assets/deformed_trees/hollow.py
index 9defe98c5..3487057eb 100644
--- a/infinigen/assets/deformed_trees/hollow.py
+++ b/infinigen/assets/deformed_trees/hollow.py
@@ -10,15 +10,16 @@
from numpy.random import uniform
from infinigen.assets.deformed_trees.base import BaseDeformedTreeFactory
-from infinigen.assets.utils.decorate import assign_material, join_objects, read_co, read_material_index, separate_loose, \
- write_material_index
+from infinigen.assets.utils.decorate import read_co, read_material_index, write_material_index
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects, separate_loose
from infinigen.assets.utils.nodegroup import geo_selection
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj, select_none
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class HollowTreeFactory(BaseDeformedTreeFactory):
diff --git a/infinigen/assets/deformed_trees/rotten.py b/infinigen/assets/deformed_trees/rotten.py
index fabf3d8b6..88b68c77d 100644
--- a/infinigen/assets/deformed_trees/rotten.py
+++ b/infinigen/assets/deformed_trees/rotten.py
@@ -9,16 +9,16 @@
from numpy.random import uniform
from infinigen.assets.deformed_trees.base import BaseDeformedTreeFactory
-from infinigen.assets.utils.decorate import assign_material, join_objects, read_material_index, remove_vertices, \
- separate_loose, write_material_index
-from infinigen.assets.utils.misc import log_uniform
-from infinigen.assets.utils.object import new_icosphere
+from infinigen.assets.utils.decorate import read_material_index, remove_vertices, write_material_index
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.object import join_objects, new_icosphere, separate_loose
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class RottenTreeFactory(BaseDeformedTreeFactory):
@staticmethod
diff --git a/infinigen/assets/deformed_trees/truncated.py b/infinigen/assets/deformed_trees/truncated.py
index 15803e704..1a398b6fc 100644
--- a/infinigen/assets/deformed_trees/truncated.py
+++ b/infinigen/assets/deformed_trees/truncated.py
@@ -14,7 +14,7 @@
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class TruncatedTreeFactory(FallenTreeFactory):
diff --git a/infinigen/assets/elements/__init__.py b/infinigen/assets/elements/__init__.py
new file mode 100644
index 000000000..765c5d999
--- /dev/null
+++ b/infinigen/assets/elements/__init__.py
@@ -0,0 +1,10 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .staircases import *
+from .doors import *
+from .rug import RugFactory
+from .warehouses import *
+from .nature_shelf_trinkets.generate import NatureShelfTrinketsFactory
+from .pillars import PillarFactory
diff --git a/infinigen/assets/elements/doors/__init__.py b/infinigen/assets/elements/doors/__init__.py
new file mode 100644
index 000000000..e0d675979
--- /dev/null
+++ b/infinigen/assets/elements/doors/__init__.py
@@ -0,0 +1,33 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from .panel import PanelDoorFactory, GlassPanelDoorFactory
+from .lite import LiteDoorFactory
+from .louver import LouverDoorFactory
+from .casing import DoorCasingFactory
+
+from infinigen.core.util import blender as butil
+
+def random_door_factory():
+ door_factories = [PanelDoorFactory, GlassPanelDoorFactory, LouverDoorFactory, LiteDoorFactory]
+ door_probs = np.array([4, 2, 3, 3])
+ return np.random.choice(door_factories, p=door_probs / door_probs.sum())
+
+
+class DoorFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(DoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.base_factory = random_door_factory()(factory_seed, coarse)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ return self.base_factory.create_asset(**params)
+
+ def finalize_assets(self, assets):
+ self.base_factory.finalize_assets(assets)
diff --git a/infinigen/assets/elements/doors/base.py b/infinigen/assets/elements/doors/base.py
new file mode 100644
index 000000000..dd44e0e8e
--- /dev/null
+++ b/infinigen/assets/elements/doors/base.py
@@ -0,0 +1,217 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials.common import unique_surface
+from infinigen.assets.utils.decorate import mirror, read_co, write_attribute, write_co
+from infinigen.assets.utils.draw import spin
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj, new_cube, new_line
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core import surface
+from infinigen.assets.materials import glass, metal, wood
+from infinigen.core.util.bevelling import add_bevel, get_bevel_edges
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+from infinigen.assets.utils.autobevel import BevelSharp
+
+class BaseDoorFactory(AssetFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super(BaseDoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = constants.DOOR_WIDTH
+ self.height = constants.DOOR_SIZE
+ self.depth = uniform(.04, .06)
+ self.panel_margin = log_uniform(.08, .12)
+ self.bevel_width = uniform(.005, .01)
+ self.out_bevel = uniform() < .7
+ self.shrink_width = log_uniform(.005, .06)
+
+ surface_fn = np.random.choice([metal, wood], p=[.2, .8])
+ self.surface = unique_surface(surface_fn, self.factory_seed)
+ self.has_glass = False
+ self.glass_surface = glass
+ self.has_louver = False
+ self.louver_surface = np.random.choice([metal, wood], p=[.2, .8])
+
+ self.handle_type = np.random.choice(['knob', 'lever', 'pull'])
+ self.handle_surface = np.random.choice([metal, wood], p=[.2, .8])
+ self.handle_offset = self.panel_margin * .5
+ self.handle_height = self.height * uniform(.45, .5)
+
+ self.knob_radius = uniform(.03, .04)
+ base_radius = uniform(1.1, 1.2)
+ mid_radius = uniform(.4, .5)
+ self.knob_radius_mid = base_radius, base_radius, mid_radius, mid_radius, 1, uniform(.6, .8), 0
+ self.knob_depth = uniform(.08, .1)
+ self.knob_depth_mid = [0, uniform(.1, .15), uniform(.25, .3), uniform(.35, .45), uniform(.6, .8), 1,
+ 1 + 1e-3]
+
+ self.lever_radius = uniform(.03, .04)
+ self.lever_mid_radius = uniform(.01, .02)
+ self.lever_depth = uniform(.05, .08)
+ self.lever_mid_depth = uniform(.15, .25)
+ self.lever_length = log_uniform(.15, .2)
+ self.level_type = np.random.choice(['wave', 'cylinder', 'bent'])
+
+ self.pull_size = log_uniform(.1, .4)
+ self.pull_depth = uniform(.05, .08)
+ self.pull_width = log_uniform(.08, .15)
+ self.pull_extension = uniform(.05, .15)
+ self.to_pull_bevel = uniform() < .5
+ self.pull_bevel_width = uniform(.02, .04)
+ self.pull_radius = uniform(.01, .02)
+ self.pull_type = np.random.choice(['u', 'tee', 'zed'])
+ self.is_pull_circular = uniform() < .5 or self.pull_type == 'zed'
+ self.panel_surface = unique_surface(surface_fn, np.random.randint(1e5))
+ self.auto_bevel = BevelSharp()
+ self.side_bevel = log_uniform(.005,.015)
+
+ self.metal_color = metal.sample_metal_color()
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ for _ in range(100):
+ obj = self._create_asset()
+ if max(obj.dimensions) < 5:
+ return obj
+ else:
+ raise ValueError('Bad door booleaning')
+
+ def _create_asset(self):
+ obj = new_cube(location=(1, 1, 1))
+ butil.apply_transform(obj, loc=True)
+ obj.scale = self.width / 2, self.depth / 2, self.height / 2
+ butil.apply_transform(obj)
+ panels = self.make_panels()
+ extras = []
+ for panel in panels:
+ extras.extend(panel['func'](obj, panel))
+ match self.handle_type:
+ case 'knob':
+ extras.extend(self.make_knobs())
+ case 'lever':
+ extras.extend(self.make_levers())
+ case 'pull':
+ extras.extend(self.make_pulls())
+ obj = join_objects([obj] + extras)
+ self.auto_bevel(obj)
+ obj.location = -self.width, -self.depth, 0
+ butil.apply_transform(obj, True)
+ obj = add_bevel(obj, get_bevel_edges(obj), offset=self.side_bevel)
+ return obj
+
+ def make_panels(self):
+ return []
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, metal_color=self.metal_color, vertical=True)
+ if self.has_glass:
+ self.glass_surface.apply(assets, selection='glass', clear=True)
+ if self.has_louver:
+ self.louver_surface.apply(assets, selection='louver', metal_color=self.metal_color)
+ self.handle_surface.apply(assets, selection='handle', metal_color='natural')
+
+ def make_knobs(self):
+ x_anchors = np.array(self.knob_radius_mid) * self.knob_radius
+ y_anchors = np.array(self.knob_depth_mid) * self.knob_depth
+ obj = spin([x_anchors, y_anchors, 0], [0, 2, 3], axis=(0, 1, 0))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.edge_face_add()
+ return self.make_handles(obj)
+
+ def make_handles(self, obj):
+ write_attribute(obj, 1, 'handle', 'FACE')
+ obj.location = self.handle_offset, 0, self.handle_height
+ butil.apply_transform(obj, loc=True)
+ other = deep_clone_obj(obj)
+ obj.location[1] += self.depth
+ butil.apply_transform(obj, loc=True)
+ mirror(other, 1)
+ return [obj, other]
+
+ def make_levers(self):
+ x_anchors = self.lever_radius, self.lever_radius, self.lever_mid_radius, self.lever_mid_radius, 0
+ y_anchors = np.array([0, self.lever_mid_depth, self.lever_mid_depth, 1, 1 + 1e-3]) * self.lever_depth
+ obj = spin([x_anchors, y_anchors, 0], [0, 1, 2, 3], axis=(0, 1, 0))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.fill()
+ lever = new_line(4)
+ if self.level_type == 'wave':
+ co = read_co(lever)
+ co[1, -1] = -uniform(.2, .3)
+ co[3, -1] = uniform(.1, .15)
+ write_co(lever, co)
+ elif self.level_type == 'bent':
+ co = read_co(lever)
+ co[4, 1] = -uniform(.2, .3)
+ write_co(lever, co)
+ lever.scale = [self.lever_length] * 3
+ butil.apply_transform(lever)
+ butil.select_none()
+ with butil.ViewportMode(lever, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, self.lever_mid_radius * 2)})
+ butil.modify_mesh(lever, 'SOLIDIFY', lever, thickness=self.lever_mid_radius, offset=0)
+ butil.modify_mesh(lever, 'SUBSURF', render_levels=1, levels=1)
+ lever.location = -self.lever_mid_radius, self.lever_depth, -self.lever_mid_radius
+ butil.apply_transform(lever, loc=True)
+ obj = join_objects([obj, lever])
+ return self.make_handles(obj)
+
+ def make_pulls(self):
+ if self.pull_type == 'u':
+ vertices = (0, 0, self.pull_size), (0, self.pull_depth, self.pull_size), (0, self.pull_depth, 0)
+ edges = (0, 1), (1, 2)
+ elif self.pull_type == 'tee':
+ vertices = (0, 0, self.pull_size), (0, self.pull_depth, self.pull_size), (0, self.pull_depth, 0), (
+ 0, self.pull_depth, self.pull_size + self.pull_extension)
+ edges = (0, 1), (1, 2), (1, 3)
+ else:
+ vertices = (0, 0, self.pull_size), (0, self.pull_depth, self.pull_size), (
+ self.pull_width, self.pull_depth, self.pull_size), (self.pull_width, self.pull_depth, 0),
+ edges = (0, 1), (1, 2), (2, 3)
+ obj = mesh2obj(data2mesh(vertices, edges))
+ butil.modify_mesh(obj, 'MIRROR', use_axis=(False, False, True))
+ if self.to_pull_bevel:
+ butil.modify_mesh(obj, 'BEVEL', width=self.pull_bevel_width, segments=4, affect='VERTICES')
+ if self.is_pull_circular:
+ surface.add_geomod(
+ obj, geo_radius, apply=True, input_args=[self.pull_radius, 32], input_kwargs={'to_align_tilt': False}
+ )
+ else:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (self.pull_radius * 2, 0, 0)})
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ obj.location = -self.pull_radius, -self.pull_radius, -self.pull_radius
+ butil.apply_transform(obj, loc=True)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.pull_radius * 2, offset=0)
+ return self.make_handles(obj)
+
+ @property
+ def casing_factory(self):
+ from infinigen.assets.elements import DoorCasingFactory
+ factory = DoorCasingFactory(self.factory_seed, self.coarse)
+ factory.surface = self.surface
+ factory.metal_color = self.metal_color
+ return factory
diff --git a/infinigen/assets/elements/doors/casing.py b/infinigen/assets/elements/doors/casing.py
new file mode 100644
index 000000000..32317fe93
--- /dev/null
+++ b/infinigen/assets/elements/doors/casing.py
@@ -0,0 +1,62 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Lingjie Mei
+
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import wood, metal
+from infinigen.assets.utils.decorate import read_edge_center, read_edge_direction
+from infinigen.assets.utils.mesh import bevel
+from infinigen.assets.utils.object import new_cube
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed
+
+
+class DoorCasingFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(DoorCasingFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.margin = constants.DOOR_SIZE * uniform(.05, .1)
+ self.extrude = uniform(.02, .08)
+ self.bevel_all_sides = uniform() < .3
+ self.surface = np.random.choice([metal, wood])
+ self.metal_color = metal.sample_metal_color()
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_cube()
+ obj.location = 0, 0, 1
+ butil.apply_transform(obj, True)
+ w = constants.DOOR_WIDTH
+ s = constants.DOOR_SIZE
+ obj.scale = w / 2 + self.margin, constants.WALL_THICKNESS / 2 + self.extrude, \
+ s / 2 + self.margin / 2
+ butil.apply_transform(obj)
+ cutter = new_cube()
+ cutter.location = 0, 0, 1 - 1e-3
+ butil.apply_transform(cutter, True)
+ cutter.scale = w / 2 - 1e-3, constants.WALL_THICKNESS + self.extrude, s / 2
+ butil.apply_transform(cutter)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+
+ x, y, z = read_edge_center(obj).T
+ x_, y_, z_ = read_edge_direction(obj).T
+
+ if self.bevel_all_sides:
+ selection = (np.abs(z_) > .5) | (np.abs(x_) > .5)
+ else:
+ selection = ((np.abs(z_) > .5) & (np.abs(x) < w / 2 + self.margin / 2)) | (
+ (np.abs(x_) > .5) & (z < s + self.margin / 2))
+ obj.data.edges.foreach_set('bevel_weight', selection)
+ bevel(obj, self.extrude, limit_method='WEIGHT')
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, metal_color=self.metal_color)
diff --git a/infinigen/assets/elements/doors/lite.py b/infinigen/assets/elements/doors/lite.py
new file mode 100644
index 000000000..e96e0a663
--- /dev/null
+++ b/infinigen/assets/elements/doors/lite.py
@@ -0,0 +1,55 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.util.math import FixedSeed
+from .panel import PanelDoorFactory
+
+
+class LiteDoorFactory(PanelDoorFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(LiteDoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ r = uniform()
+ subdivide_glass = False
+ if r <= 1 / 6:
+ dimension = 0, 1, uniform(.4, .6), 1
+ subdivide_glass = True
+ elif r <= 1 / 3:
+ dimension = 0, 1, 0, 1
+ subdivide_glass = True
+ elif r <= 1 / 2:
+ dimension = 0, uniform(.3, .4), uniform(.4, .6), 1
+ elif r <= 2 / 3:
+ dimension = 0, uniform(.3, .4), uniform(.4, .6), 1
+ elif r <= 5 / 6:
+ dimension = 0, 1, 0, 1
+ else:
+ x = uniform(.3, .35)
+ dimension = x, 1 - x, uniform(.7, .8), 1
+ self.x_min, self.x_max, self.y_min, self.y_max = dimension
+ if subdivide_glass:
+ self.x_subdivisions = np.random.choice([1, 3])
+ self.y_subdivisions = int(self.height / self.width * self.x_subdivisions) + np.random.randint(
+ -1, 2
+ )
+ else:
+ self.x_subdivisions = 1
+ self.y_subdivisions = 1
+ self.has_glass = True
+
+ def make_panels(self):
+ x_range = np.linspace(self.x_min, self.x_max, self.x_subdivisions + 1) * (
+ self.width - self.panel_margin * 2) + self.panel_margin
+ y_range = np.linspace(self.y_min, self.y_max, self.y_subdivisions + 1) * (
+ self.height - self.panel_margin * 2) + self.panel_margin
+ panels = []
+ for x_min, x_max in zip(x_range[:-1], x_range[1:]):
+ for y_min, y_max in zip(y_range[:-1], y_range[1:]):
+ panels.append(
+ {'dimension': (x_min, x_max, y_min, y_max), 'func': self.bevel, 'attribute_name': 'glass'}
+ )
+ return panels
diff --git a/infinigen/assets/elements/doors/louver.py b/infinigen/assets/elements/doors/louver.py
new file mode 100644
index 000000000..48f8bee6e
--- /dev/null
+++ b/infinigen/assets/elements/doors/louver.py
@@ -0,0 +1,80 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from .panel import PanelDoorFactory
+from infinigen.assets.utils.decorate import write_attribute, write_co
+from infinigen.assets.utils.object import new_cube, new_plane
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+
+class LouverDoorFactory(PanelDoorFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(LouverDoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.x_subdivisions = 1
+ self.y_subdivisions = np.clip(np.random.binomial(5, .4), 1, None)
+ self.has_panel = uniform() < .7
+ self.has_upper_panel = uniform() < .5
+ self.louver_width = uniform(.002, .004)
+ self.louver_margin = uniform(.02, .03)
+ self.louver_size = log_uniform(.05, .1)
+ self.louver_angle = uniform(np.pi / 4.5, np.pi / 3.5)
+ self.has_louver = True
+
+ def louver(self, obj, panel):
+ x_min, x_max, y_min, y_max = panel['dimension']
+ cutter = new_cube(location=(1, 1, 1))
+ butil.apply_transform(cutter, loc=True)
+ write_attribute(cutter, 1, 'louver', 'FACE')
+ cutter.location = x_min - self.louver_margin, -self.louver_width, y_min - self.louver_margin
+ cutter.scale = [(x_max - x_min) / 2 + self.louver_margin, self.depth / 2 + self.louver_width,
+ (y_max - y_min) / 2 + self.louver_margin]
+ butil.apply_transform(cutter, loc=True)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+
+ hole = new_cube(location=(1, 1, 1))
+ butil.apply_transform(hole, loc=True)
+ write_attribute(hole, 1, 'louver', 'FACE')
+ hole.location = x_min, -self.louver_width * 2, y_min
+ hole.scale = (x_max - x_min) / 2, self.depth / 2 + self.louver_width * 2, (y_max - y_min) / 2
+ butil.apply_transform(hole, loc=True)
+ butil.modify_mesh(cutter, 'BOOLEAN', object=hole, operation='DIFFERENCE')
+ butil.delete(hole)
+
+ louver = new_plane()
+ x = x_min, x_max, x_min, x_max
+ y = 0, 0, self.depth, self.depth
+ y_upper = y_min + self.depth * np.tan(self.louver_angle)
+ z = y_min, y_min, y_upper, y_upper
+ write_co(louver, np.stack([x, y, z], -1))
+ butil.modify_mesh(louver, 'SOLIDIFY', thickness=self.louver_width, offset=0)
+ butil.modify_mesh(
+ louver, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ constant_offset_displace=(0, 0, self.louver_size),
+ count=int(np.ceil((y_max - y_min) / self.louver_size) + .5)
+ )
+ write_attribute(louver, 1, 'louver', 'FACE')
+ return [cutter, louver]
+
+ def make_panels(self):
+ panels = super(LouverDoorFactory, self).make_panels()
+ if len(panels) == 1:
+ panels[0]['func'] = self.louver
+ elif len(panels) == 2:
+ if not self.has_panel:
+ panels[0]['func'] = self.louver
+ panels[1]['func'] = self.louver
+ else:
+ if self.has_upper_panel:
+ panels = [panels[0], panels[-1]]
+ else:
+ panels = [panels[0]]
+ for panel in panels:
+ panel['func'] = self.louver
+ return panels
diff --git a/infinigen/assets/elements/doors/panel.py b/infinigen/assets/elements/doors/panel.py
new file mode 100644
index 000000000..eb73f1434
--- /dev/null
+++ b/infinigen/assets/elements/doors/panel.py
@@ -0,0 +1,92 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core import surface
+from infinigen.core.surface import write_attr_data, read_attr_data
+from .casing import DoorCasingFactory
+from infinigen.assets.elements.doors.base import BaseDoorFactory
+from infinigen.assets.utils.decorate import write_attribute, select_faces, read_area
+from infinigen.assets.utils.object import new_cube
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+
+
+class PanelDoorFactory(BaseDoorFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(PanelDoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.x_subdivisions = 1 if uniform() < .5 else 2
+ self.y_subdivisions = np.clip(np.random.binomial(5, .45), 1, None)
+
+ def bevel(self, obj, panel):
+ x_min, x_max, y_min, y_max = panel['dimension']
+ assert x_min <= x_max and y_min <= y_max
+ cutter = new_cube()
+ butil.apply_transform(cutter, loc=True)
+ if panel['attribute_name'] is not None:
+ write_attribute(cutter, 1, panel['attribute_name'], 'FACE')
+ cutter.location = (x_max + x_min) / 2, self.bevel_width * .5 - .1, (y_max + y_min) / 2
+ cutter.scale = (x_max - x_min) / 2 - 2e-3, .1, (y_max - y_min) / 2 - 2e-3
+ butil.apply_transform(cutter, loc=True)
+ # butil.modify_mesh(cutter, 'BEVEL', width=self.bevel_width)
+ write_attr_data(cutter, 'cut', np.ones(len(cutter.data.polygons), dtype=int), 'INT', 'FACE')
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ cutter.location[1] += .2 + self.depth - self.bevel_width
+ butil.apply_transform(cutter, loc=True)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+ select_faces(obj, (read_area(obj) > .01) & (read_attr_data(obj, 'cut')))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.inset(thickness=self.shrink_width)
+ bpy.ops.mesh.inset(thickness=self.bevel_width, depth=self.bevel_width)
+ obj.data.attributes.remove(obj.data.attributes['cut'])
+ return []
+
+ def make_panels(self):
+ panels = []
+ x_cuts = np.random.randint(1, 4, self.x_subdivisions)
+ x_cuts = np.cumsum(x_cuts / x_cuts.sum())
+ y_cuts = np.sort(np.random.randint(2, 5, self.y_subdivisions))[::-1]
+ y_cuts = np.cumsum(y_cuts / y_cuts.sum())
+ for j in range(len(y_cuts)):
+ for i in range(len(x_cuts)):
+ x_min = self.panel_margin + (self.width - self.panel_margin) * (x_cuts[i - 1] if i > 0 else 0)
+ x_max = (self.width - self.panel_margin) * x_cuts[i]
+ y_min = self.panel_margin + (self.height - self.panel_margin) * (y_cuts[j - 1] if j > 0 else 0)
+ y_max = (self.height - self.panel_margin) * y_cuts[j]
+ panels.append(
+ {'dimension': (x_min, x_max, y_min, y_max), 'func': self.bevel, 'attribute_name': None}
+ )
+ return panels
+
+
+class GlassPanelDoorFactory(PanelDoorFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super(GlassPanelDoorFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.x_subdivisions = 2
+ self.y_subdivisions = np.clip(np.random.binomial(5, .5), 2, None)
+ self.merge_glass = self.y_subdivisions < 4
+ self.has_glass = True
+
+ def make_panels(self):
+ panels = super(GlassPanelDoorFactory, self).make_panels()
+ if self.merge_glass:
+ first_dimension = panels[-self.x_subdivisions]['dimension']
+ last_dimension = panels[- 1]['dimension']
+ merged = {
+ 'dimension': (first_dimension[0], last_dimension[1], first_dimension[2], last_dimension[3]),
+ 'func': self.bevel,
+ 'attribute_name': 'glass'
+ }
+ return [merged, *panels[:self.x_subdivisions]]
+ else:
+ for panel in panels[-self.x_subdivisions:]:
+ panel['attribute_name'] = 'glass'
+ return panels
diff --git a/infinigen/assets/elements/nature_shelf_trinkets/generate.py b/infinigen/assets/elements/nature_shelf_trinkets/generate.py
new file mode 100644
index 000000000..a7a2bc717
--- /dev/null
+++ b/infinigen/assets/elements/nature_shelf_trinkets/generate.py
@@ -0,0 +1,98 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Stamatis Alexandropulos
+
+import colorsys
+
+import bpy
+import numpy as np
+import trimesh
+import mathutils
+from numpy.random import uniform
+
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.assets.corals import CoralFactory
+from infinigen.assets.rocks import BlenderRockFactory
+from infinigen.assets.rocks.boulder import BoulderFactory
+from infinigen.assets.mollusk import MolluskFactory, AugerFactory, ClamFactory, ConchFactory, MusselFactory, ScallopFactory, VoluteFactory
+from infinigen.assets.monocot import PineconeFactory
+from infinigen.assets.creatures.beetle import BeetleFactory, AntSwarmFactory
+from infinigen.assets.creatures.bird import BirdFactory, FlyingBirdFactory
+from infinigen.assets.creatures.carnivore import CarnivoreFactory
+from infinigen.assets.creatures.herbivore import HerbivoreFactory
+from infinigen.assets.creatures.crustacean import CrustaceanFactory, CrabFactory, LobsterFactory, SpinyLobsterFactory
+from infinigen.assets.creatures.reptile import FrogFactory
+from infinigen.assets.creatures.insects.dragonfly import DragonflyFactory
+from infinigen.assets.utils.decorate import remove_vertices
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils import object as obj
+from infinigen.assets.utils.object import join_objects
+
+
+
+
+class NatureShelfTrinketsFactory(AssetFactory):
+ factories = [CoralFactory,BlenderRockFactory, BoulderFactory, PineconeFactory, MolluskFactory,
+ AugerFactory, ClamFactory, ConchFactory, MusselFactory, ScallopFactory, VoluteFactory, CarnivoreFactory, HerbivoreFactory]
+ probs = np.array([1,1,1,1,3,2,3,2,2,2,2,5,5])
+
+ def __init__(self, factory_seed, coarse=False):
+ super(NatureShelfTrinketsFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ base_factory_fn = np.random.choice(self.factories, p=self.probs / self.probs.sum())
+
+ kwargs = {}
+ if base_factory_fn in [HerbivoreFactory, CarnivoreFactory]:
+ kwargs.update({
+ 'hair': False
+ })
+
+ self.base_factory = base_factory_fn(self.factory_seed, **kwargs)
+
+
+ def create_placeholder(self, **params) -> bpy.types.Object:
+ size = np.random.uniform(0.1, 0.15)
+ bpy.ops.mesh.primitive_cube_add(size=size, location=(0,0, size/2))
+ placeholder = bpy.context.active_object
+ return placeholder
+
+
+ def create_asset(self, i, placeholder=None, **params):
+ asset = self.base_factory.spawn_asset(
+ np.random.randint(1e7),
+ distance=200,
+ adaptive_resolution = False
+ )
+
+ if (list(asset.children)):
+ asset = join_objects(list(asset.children))
+
+ # butil.modify_mesh(asset, 'DECIMATE')
+ butil.apply_transform(asset,loc=True)
+ butil.apply_modifiers(asset)
+ if isinstance(self.base_factory, HerbivoreFactory) or isinstance(self.base_factory, CarnivoreFactory):
+ pass
+ else:
+ if not isinstance(asset, trimesh.Trimesh):
+ mesh = obj.obj2trimesh(asset)
+ stable_poses, probs = trimesh.poses.compute_stable_poses(mesh)
+ stable_pose = stable_poses[np.argmax(probs)]
+ asset.rotation_euler = mathutils.Matrix(stable_pose[:3,:3]).to_euler()
+ butil.apply_transform(asset,rot =True)
+ dim = asset.dimensions
+ bounding_box = placeholder.dimensions
+ scale = min([bounding_box[i]/dim[i] for i in range(3)])
+ asset.scale = [scale for i in range(3)]
+ # asset.dimensions = placeholder.dimensions
+ butil.apply_transform(asset,loc=True)
+ bounds = butil.bounds(asset)
+ cur_loc = asset.location
+ new_location = [
+ cur_loc[i]-(bounds[0][i] + bounds[1][i])/2 for i in range(3)]
+ new_location[2] = cur_loc[2] - (bounds[0][2] + bounding_box[2]/2)
+ asset.location = new_location
+ butil.apply_transform(asset,loc=True)
+ return asset
\ No newline at end of file
diff --git a/infinigen/assets/elements/pillars.py b/infinigen/assets/elements/pillars.py
new file mode 100644
index 000000000..8507f1931
--- /dev/null
+++ b/infinigen/assets/elements/pillars.py
@@ -0,0 +1,123 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bmesh
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import marble_regular, marble_voronoi
+from infinigen.assets.utils.decorate import (
+ read_co, read_edge_center, read_selected, select_edges,
+ subdivide_edge_ring, subsurf, write_co,
+)
+from infinigen.assets.utils.object import (
+ join_objects, new_base_circle, new_base_cylinder, new_circle,
+ new_cylinder,
+)
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import log_uniform
+
+
+class PillarFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.height = constants.WALL_HEIGHT - constants.WALL_THICKNESS
+ self.n = np.random.randint(5, 10)
+ self.radius = uniform(.08, .12)
+ self.outer_radius = self.radius * uniform(1.3, 1.5)
+ self.lower_offset = uniform(.05, .15)
+ self.upper_offset = uniform(.05, .15)
+ self.detail_type = np.random.choice(['fluting', 'reeding'])
+ width = np.pi / 2 / self.n
+ self.inset_width = width * log_uniform(.1, .2)
+ self.inset_width_ = (width - self.inset_width * 2) * uniform(-.1, .3)
+ self.inset_depth = uniform(.1, .15)
+ self.inset_scale = uniform(.05, .1)
+ self.outer_n = np.random.choice([1, 2, self.n])
+ self.m = np.random.randint(12, 20)
+ z_profile = uniform(1, 3, self.m)
+ self.z_profile = np.array([0, *(np.cumsum(z_profile) / np.sum(z_profile))[:-1]])
+ alpha = uniform(.7, .85)
+ r_profile = uniform(0, 1, self.m + 3)
+ r_profile[[0, 1]] = 1
+ r_profile[[-2, -1]] = 0
+ r_profile = np.convolve(r_profile, np.array([(1 - alpha) / 2, alpha, (1 - alpha) / 2]))
+ self.r_profile = np.array([1, *r_profile[2:-2]]) * (self.outer_radius - self.radius) + self.radius
+ self.n_profile = np.where(
+ np.arange(self.m) < np.random.randint(2, self.m - 1), self.outer_n,
+ self.n
+ )
+ self.inset_profile = uniform(0, 1, self.m) < .3
+ self.surface = np.random.choice([marble_regular, marble_voronoi])
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_cylinder(vertices=4 * self.n)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if len(f.verts) > 4]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_ONLY')
+ bmesh.update_edit_mesh(obj.data)
+
+ obj.scale = self.radius, self.radius, (1 - self.lower_offset - self.upper_offset) * self.height
+ obj.location[-1] = self.lower_offset * self.height
+ butil.apply_transform(obj, True)
+ inset_scale = 1 + self.inset_scale * (1 if self.detail_type == 'reeding' else -1)
+ if self.detail_type in ['fluting', 'reeding']:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.inset(thickness=self.inset_width * self.radius, use_individual=True)
+ bpy.ops.mesh.inset(thickness=self.inset_width_ * self.radius, use_individual=True)
+ bpy.ops.transform.resize(value=(inset_scale, inset_scale, 1))
+ subdivide_edge_ring(obj, 16)
+ parts = [obj]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ z_rot = np.pi / 2 * np.random.randint(2)
+ for z, r, n, i in zip(self.z_profile, self.r_profile, self.n_profile, self.inset_profile):
+ o = new_base_circle(vertices=4 * n)
+ if i:
+ co = read_co(o)
+ stride = np.random.choice([2, 4, 8])
+ co *= np.where(np.arange(len(co)) % stride == 0, 1, inset_scale)[:, np.newaxis]
+ write_co(o, co)
+ with butil.ViewportMode(o, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.subdivide(number_cuts=self.n // n - 1)
+ o.location[-1] = z * self.lower_offset * self.height
+ r_ = r / np.cos(np.pi / 4 / n)
+ o.scale = r_, r_, 1
+ o.rotation_euler[-1] = z_rot
+ o_ = deep_clone_obj(o)
+ o_.location[-1] = (1 - z * self.upper_offset) * self.height
+ butil.apply_transform(o, True)
+ butil.apply_transform(o_, True)
+ parts.extend([o, o_])
+ obj = join_objects(parts)
+ selection = read_selected(obj, 'EDGE')
+ z = read_edge_center(obj)[:, -1]
+ number_cuts = 0
+ smoothness = uniform(1, 1.4)
+ select_edges(obj, selection & (z < .5))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.bridge_edge_loops(number_cuts=number_cuts, smoothness=smoothness)
+ select_edges(obj, selection & (z > .5))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.bridge_edge_loops(number_cuts=number_cuts, smoothness=smoothness)
+ subsurf(obj, 1, True)
+ subsurf(obj, 1)
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/elements/rug.py b/infinigen/assets/elements/rug.py
new file mode 100644
index 000000000..0f5072902
--- /dev/null
+++ b/infinigen/assets/elements/rug.py
@@ -0,0 +1,64 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import rug
+from infinigen.assets.materials.art import Art, ArtRug
+from infinigen.assets.utils.object import new_bbox, new_circle, new_plane, new_base_circle
+from infinigen.assets.utils.uv import wrap_sides
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform, clip_gaussian
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class RugFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(RugFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = clip_gaussian(3, 1, 2, 6)
+ self.length = self.width * uniform(1, 1.5)
+ self.rug_shape = np.random.choice(['rectangle', 'circle', 'rounded', 'ellipse'])
+ if self.rug_shape == 'circle':
+ self.length = self.width
+ self.rounded_buffer = self.width * uniform(.1, .5)
+ self.thickness = uniform(.01, .02)
+ material_assignments = AssetList['RugFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ if self.surface == ArtRug:
+ self.surface = self.surface(self.factory_seed)
+
+ def build_shape(self):
+ match self.rug_shape:
+ case 'rectangle':
+ obj = new_plane()
+ obj.scale = self.length / 2, self.width / 2, 1
+ butil.apply_transform(obj, True)
+ case 'rounded':
+ obj = new_plane()
+ obj.scale = self.length / 2, self.width / 2, 1
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'BEVEL', width=self.rounded_buffer, segments=16)
+ case _:
+ obj = new_base_circle(vertices=128)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.edge_face_add()
+ obj.scale = self.length / 2, self.width / 2, 1
+ butil.apply_transform(obj, True)
+ return obj
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(-self.length / 2, self.length / 2, -self.width / 2, self.width / 2, 0, self.thickness)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.build_shape()
+ wrap_sides(obj, self.surface, 'z', 'x', 'y')
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=1)
+ return obj
diff --git a/infinigen/assets/elements/staircases/__init__.py b/infinigen/assets/elements/staircases/__init__.py
new file mode 100644
index 000000000..0558c4205
--- /dev/null
+++ b/infinigen/assets/elements/staircases/__init__.py
@@ -0,0 +1,18 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from .curved import CurvedStaircaseFactory
+from .spiral import SpiralStaircaseFactory
+from .straight import StraightStaircaseFactory
+from .l_shaped import LShapedStaircaseFactory
+from .u_shaped import UShapedStaircaseFactory
+from .cantilever import CantileverStaircaseFactory
+
+
+def random_staircase_factory():
+ door_factories = [StraightStaircaseFactory, LShapedStaircaseFactory, UShapedStaircaseFactory,
+ SpiralStaircaseFactory, CurvedStaircaseFactory, CantileverStaircaseFactory]
+ door_probs = np.array([2, 2, 2, .5, 2, 2])
+ return np.random.choice(door_factories, p=door_probs / door_probs.sum())
diff --git a/infinigen/assets/elements/staircases/cantilever.py b/infinigen/assets/elements/staircases/cantilever.py
new file mode 100644
index 000000000..581a28672
--- /dev/null
+++ b/infinigen/assets/elements/staircases/cantilever.py
@@ -0,0 +1,32 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+import shapely
+import shapely.affinity
+from infinigen.assets.elements.staircases.straight import StraightStaircaseFactory
+from infinigen.assets.utils.decorate import read_co
+from infinigen.assets.utils.object import join_objects
+
+from infinigen.core.util import blender as butil
+
+
+class CantileverStaircaseFactory(StraightStaircaseFactory):
+ support_types = 'wall'
+ handrail_types = 'weighted_choice', (2, 'horizontal-post'), (2, 'vertical-post')
+
+ def valid_contour(self, offset, contour, doors, lower=True):
+ valid = super().valid_contour(offset, contour, doors, lower)
+ if not valid or not lower:
+ return valid
+ obj = join_objects([self.make_line_offset(0), self.make_line_offset(1)])
+ co = read_co(obj)[:, :-1]
+ butil.delete(obj)
+ if self.mirror:
+ co[:, 0] = -co[:, 0]
+ points = [shapely.affinity.translate(shapely.affinity.rotate(p, self.rot_z, (0, 0)), *offset) for p in
+ shapely.points(co)]
+ others = [shapely.ops.nearest_points(p, contour.boundary)[0] for p in points]
+ distance = np.array([np.abs(p.x - o.x) + np.abs(p.y - o.y) for p, o in zip(points, others)])
+ return (distance < .1).sum() / len(distance) > .5
diff --git a/infinigen/assets/elements/staircases/curved.py b/infinigen/assets/elements/staircases/curved.py
new file mode 100644
index 000000000..2190e1af3
--- /dev/null
+++ b/infinigen/assets/elements/staircases/curved.py
@@ -0,0 +1,67 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Karhan Kayan: fix constants
+
+import numpy as np
+
+from infinigen.assets.elements.staircases.straight import StraightStaircaseFactory
+from infinigen.assets.utils.decorate import read_co, write_co
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+
+
+class CurvedStaircaseFactory(StraightStaircaseFactory):
+ support_types = 'weighted_choice', (2, 'single-rail'), (2, 'double-rail'), (4, 'side'), (4, 'solid'), (
+ 4, 'hole')
+
+ handrail_types = 'weighted_choice', (2, 'horizontal-post'), (2, 'vertical-post')
+
+ def __init__(self, factory_seed, coarse=False):
+ self.full_angle, self.radius, self.theta = 0, 0, 0
+ super(CurvedStaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.has_spiral = True
+
+ def build_size_config(self):
+ while True:
+ self.full_angle = np.random.randint(1, 5) * np.pi / 2
+ self.n = np.random.randint(13, 21)
+ self.step_height = constants.WALL_HEIGHT / self.n
+ self.theta = self.full_angle / self.n
+ self.step_length = self.step_height * log_uniform(1, 1.5)
+ self.step_width = log_uniform(.9, 1.5)
+ self.radius = self.step_length / self.theta
+ if self.radius / self.step_width > 1.5:
+ break
+
+ def make_spiral(self, obj):
+ x, y, z = read_co(obj).T
+ u = x + self.radius - self.step_width
+ t = y / self.step_length * self.theta
+ write_co(obj, np.stack([u * np.cos(t), u * np.sin(t), z], -1))
+
+ def unmake_spiral(self, obj):
+ co = read_co(obj)
+ x, y, z = co.T
+ u = np.linalg.norm(co[:, :2], axis=-1)
+ t = np.arctan2(y, x)
+ margins, ts = [], []
+ for o in np.linspace(0, np.pi * 2, 8):
+ t_ = (t - o) % (np.pi * 2) + o
+ margins.append(np.max(t_) - np.min(t_))
+ ts.append(t_)
+ t = ts[np.argmin(margins)]
+ x = u - self.radius + self.step_width
+ y = t * self.step_length / self.theta
+ co = np.stack([x, y, z], -1)
+ write_co(obj, co)
+ return obj
+
+ @property
+ def upper(self):
+ return np.pi / 2 + self.full_angle
diff --git a/infinigen/assets/elements/staircases/generate.py b/infinigen/assets/elements/staircases/generate.py
new file mode 100644
index 000000000..13e12ffef
--- /dev/null
+++ b/infinigen/assets/elements/staircases/generate.py
@@ -0,0 +1,33 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from .straight import StraightStaircaseFactory
+from .l_shaped import LShapedStaircaseFactory
+from .u_shaped import UShapedStaircaseFactory
+from .cantilever import CantileverStaircaseFactory
+from .spiral import SpiralStaircaseFactory
+from .curved import CurvedStaircaseFactory
+
+
+class StaircaseFactory(AssetFactory):
+ factories = [StraightStaircaseFactory, LShapedStaircaseFactory, UShapedStaircaseFactory,
+ SpiralStaircaseFactory, CurvedStaircaseFactory, CantileverStaircaseFactory]
+ probs = np.array([4, 3, 3, 1, 2, 2])
+
+ def __init__(self, factory_seed, coarse=False):
+ super(StaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ base_factory_fn = np.random.choice(self.factories, p=self.probs / self.probs.sum())
+ self.base_factory = base_factory_fn(self.factory_seed)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ return self.base_factory.create_asset(**params)
+
+ def finalize_assets(self, assets):
+ self.base_factory.finalize_assets(assets)
diff --git a/infinigen/assets/elements/staircases/l_shaped.py b/infinigen/assets/elements/staircases/l_shaped.py
new file mode 100644
index 000000000..78014290e
--- /dev/null
+++ b/infinigen/assets/elements/staircases/l_shaped.py
@@ -0,0 +1,145 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from .straight import StraightStaircaseFactory
+from infinigen.assets.utils.decorate import read_co, write_attribute, write_co
+from infinigen.assets.utils.object import new_cube, new_line
+from infinigen.core.util.math import FixedSeed
+import infinigen.core.util.blender as butil
+
+
+class LShapedStaircaseFactory(StraightStaircaseFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(LShapedStaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.m = int(self.n * uniform(.4, .6))
+ self.is_rail_circular = True
+
+ def make_line(self, alpha):
+ obj = new_line(self.n + 2)
+ x = np.concatenate(
+ [np.full(self.m + 2, alpha * self.step_width), -np.arange(self.n - self.m + 1) * self.step_length]
+ )
+ y = np.concatenate(
+ [np.arange(self.m + 1) * self.step_length, [self.m * self.step_length + alpha * self.step_width],
+ np.full(self.n - self.m + 1, self.m * self.step_length + alpha * self.step_width)]
+ )
+ z = np.concatenate([np.arange(self.m + 1), [self.m], np.arange(self.m, self.n + 1)]) * self.step_height
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_line_offset(self, alpha):
+ obj = self.make_line(alpha)
+ co = read_co(obj)
+ co[self.m:self.m + 2] = co[self.m + 1:self.m + 3]
+ x, y, z = co.T
+ x[self.m + 1] += min(self.step_length / 2, alpha * self.step_width)
+ x[self.m + 2:] -= self.step_length / 2
+ y[:self.m] += self.step_length / 2
+ z += self.step_height
+ z[[self.m, self.m + 1, - 1]] -= self.step_height
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.m - 1)
+ chunks_ = self.split(self.m + 1, self.n + 2)
+ indices = list(c[0] for c in chunks) + [self.m - 1, self.m, self.m + 1] + list(
+ c[0] for c in chunks_
+ ) + [self.n + 1, self.n + 2]
+ return cos[indices]
+
+ def make_vertical_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.m - 1)
+ chunks_ = self.split(self.m + 1, self.n + 2)
+ indices = sum(list(c[1:].tolist() for c in chunks), [])
+ indices_ = sum(list(c[1:].tolist() for c in chunks_), [])
+ mid_cos = []
+ mid = [self.m - 1, self.m]
+ for m in mid:
+ for r in np.linspace(0, 1, self.post_k + 1 if m >= self.m else self.post_k + 2)[1:-1]:
+ mid_cos.append(r * cos[m] + (1 - r) * cos[m + 1])
+ return np.concatenate([cos[indices], np.stack(mid_cos), cos[indices_]], 0)
+
+ def make_steps(self):
+ objs = super(LShapedStaircaseFactory, self).make_steps()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi / 2
+ obj.location = self.m * self.step_length, self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+ lowest = np.min(read_co(objs[self.m]).T[-1])
+ platform = new_cube(location=(1, 1, 1))
+ butil.apply_transform(platform, loc=True)
+ platform.location = 0, self.step_length * self.m, lowest
+ platform.scale = self.step_width / 2, self.step_width / 2, (self.step_height * self.m - lowest) / 2
+ butil.apply_transform(platform, loc=True)
+ write_attribute(platform, 1, 'steps', 'FACE')
+ return objs + [platform]
+
+ def make_treads(self):
+ objs = super(LShapedStaircaseFactory, self).make_treads()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi / 2
+ obj.location = self.m * self.step_length, self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+ platform = new_cube(location=(1, 1, 1))
+ butil.apply_transform(platform, loc=True)
+ platform.location = 0, self.step_length * self.m, self.step_height * self.m
+ platform.scale = self.step_width / 2, self.step_width / 2, self.tread_height / 2
+ butil.apply_transform(platform, loc=True)
+ write_attribute(platform, 1, 'treads', 'FACE')
+ return objs + [platform]
+
+ def make_inner_sides(self):
+ objs = super(LShapedStaircaseFactory, self).make_inner_sides()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi / 2
+ obj.location = self.m * self.step_length, self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+
+ top_cutter = new_cube(location=(0, 0, 1))
+ butil.apply_transform(top_cutter, loc=True)
+ top_cutter.scale = [100] * 3
+ top_cutter.location[-1] = self.m * self.step_height + self.tread_height
+ for obj in objs[:self.m]:
+ butil.modify_mesh(obj, 'BOOLEAN', object=top_cutter, operation='DIFFERENCE')
+ butil.delete(top_cutter)
+ return objs
+
+ def make_outer_sides(self):
+ objs = self.make_inner_sides()
+ for obj in objs[:self.m]:
+ obj.location[0] += self.step_width
+ butil.apply_transform(obj, loc=True)
+ for obj in objs[self.m:]:
+ obj.location[1] += self.step_width
+ butil.apply_transform(obj, loc=True)
+ platform = new_line(2)
+ x = self.step_width, self.step_width, 0
+ y = self.m * self.step_length, self.m * self.step_length + self.step_width, self.m * self.step_length \
+ + self.step_width
+ z = [self.m * self.step_height] * 3
+ write_co(platform, np.stack([x, y, z], -1))
+ butil.select_none()
+ with butil.ViewportMode(platform, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, -self.side_height)})
+ butil.modify_mesh(platform, 'SOLIDIFY', thickness=self.side_thickness)
+ write_attribute(platform, 1, 'sides', 'FACE')
+ return objs + [platform]
+
+ @property
+ def upper(self):
+ return np.pi
diff --git a/infinigen/assets/elements/staircases/spiral.py b/infinigen/assets/elements/staircases/spiral.py
new file mode 100644
index 000000000..6d5a75c9d
--- /dev/null
+++ b/infinigen/assets/elements/staircases/spiral.py
@@ -0,0 +1,62 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Karhan Kayan: fix constants
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.elements.staircases.curved import CurvedStaircaseFactory
+from infinigen.assets.utils.decorate import read_co, remove_vertices, write_attribute
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import new_line, separate_loose
+from infinigen.core import surface
+from infinigen.core.util.math import FixedSeed
+import infinigen.core.util.blender as butil
+
+
+class SpiralStaircaseFactory(CurvedStaircaseFactory):
+ support_types = 'column'
+
+ def __init__(self, factory_seed, coarse=False):
+ super(SpiralStaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.column_radius = self.radius - self.step_width + uniform(.05, .08)
+ self.has_column = True
+ self.handrail_alphas = [1 - self.handrail_offset / self.step_width]
+
+ def build_size_config(self):
+ while True:
+ self.full_angle = np.random.randint(1, 5) * np.pi / 2
+ self.n = np.random.randint(13, 21)
+ self.step_height = constants.WALL_HEIGHT / self.n
+ self.theta = self.full_angle / self.n
+ self.step_length = self.step_height * log_uniform(1, 1.2)
+ self.radius = self.step_length / self.theta
+ if .9 < self.radius < 1.5:
+ self.step_width = self.radius * uniform(.9, .95)
+ break
+
+ def make_column(self):
+ obj = new_line(self.n, self.step_height * self.n + self.post_height)
+ obj.rotation_euler[1] = - np.pi / 2
+ butil.apply_transform(obj)
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[self.column_radius, 16])
+ write_attribute(obj, 1, 'steps', 'FACE')
+ return obj
+
+ def unmake_spiral(self, obj):
+ obj = super().unmake_spiral(obj)
+ x, y, z = read_co(obj).T
+ margin = .1
+ if (x >= 0).sum() >= (x <= 0).sum():
+ remove_vertices(obj, lambda x, y, z: x < margin)
+ else:
+ remove_vertices(obj, lambda x, y, z: x > -margin)
+ obj = separate_loose(obj)
+ return obj
diff --git a/infinigen/assets/elements/staircases/straight.py b/infinigen/assets/elements/staircases/straight.py
new file mode 100644
index 000000000..11a204524
--- /dev/null
+++ b/infinigen/assets/elements/staircases/straight.py
@@ -0,0 +1,604 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Karhan Kayan: fix constants
+
+import bpy
+import bmesh
+import gin
+import numpy as np
+import shapely
+from numpy.random import uniform
+from shapely import LineString, Polygon
+
+from infinigen.assets.materials.stone_and_concrete import concrete
+from infinigen.assets.utils.mesh import canonicalize_ls, convert2ls
+from infinigen.assets.utils.shapes import cut_polygon_by_line
+from infinigen.assets.materials import metal, glass, plaster, wood, fabrics
+from infinigen.assets.utils.decorate import (
+ mirror, read_co, remove_faces, remove_vertices, subsurf,
+ write_attribute, write_co,
+)
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import (
+ data2mesh, join_objects, mesh2obj, new_circle, new_cube, new_line,
+ separate_loose,
+)
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.placement.detail import sharp_remesh_with_attrs
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core import surface
+from infinigen.core.surface import read_attr_data, write_attr_data
+from infinigen.core.tagging import PREFIX
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed, normalize
+from infinigen.core.util.random import log_uniform, random_general as rg
+
+from infinigen.core import tags as t
+
+
+class StraightStaircaseFactory(AssetFactory):
+ support_types = 'weighted_choice', (2, 'single-rail'), (2, 'double-rail'), (3, 'side'), (3, 'solid'), (
+ 3, 'hole')
+ handrail_types = 'weighted_choice', (2, 'glass'), (2, 'horizontal-post'), (2, 'vertical-post')
+
+ def __init__(self, factory_seed, coarse=False):
+ super(StraightStaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.support_type = rg(self.support_types)
+ self.n, self.step_height, self.step_width, self.step_length = 0, 0, 0, 0
+ self.build_size_config()
+
+ self.has_step = self.support_type in ['solid', 'hole']
+ self.hole_size = log_uniform(.6, 1.)
+ probs = np.array([3, 2, 2, 2])
+ self.step_surface = np.random.choice([wood, plaster, concrete, fabrics], p=probs / probs.sum())
+
+ self.has_rail = self.support_type in ['single-rail', 'double-rail']
+ self.rail_offset = self.step_width * uniform(.15, .3)
+ self.is_rail_circular = uniform() < .5
+ self.rail_width = log_uniform(.08, .2)
+ self.rail_height = log_uniform(.08, .12)
+ probs = np.array([3, 2, 2, 1])
+ self.rail_surface = np.random.choice([metal, plaster, concrete, fabrics], p=probs / probs.sum())
+
+ self.has_tread = not self.has_step or uniform() < .75
+ self.tread_height = uniform(.01, .02) if self.has_step else uniform(.06, .08)
+ self.tread_length = self.step_length + uniform(.01, .02)
+ self.tread_width = self.step_width + uniform(.01, .02) if uniform() < .8 else self.step_width
+ probs = np.array([3, 3, 1])
+ self.tread_surface = np.random.choice([wood, metal, glass], p=probs / probs.sum())
+
+ self.has_sides = self.support_type in ['side', 'solid', 'hole']
+ self.side_type = np.random.choice(['zig-zag', 'straight'])
+ self.side_height = self.step_height * log_uniform(.2, .8)
+ self.side_thickness = uniform(.03, .08)
+ probs = np.array([3, 3, 1, 2])
+ self.side_surface = np.random.choice([wood, metal, plaster, fabrics], p=probs / probs.sum())
+
+ self.has_column = self.support_type == 'chord'
+
+ self.handrail_type = rg(self.handrail_types)
+ self.is_handrail_circular = uniform() < .7
+ self.handrail_width = log_uniform(.02, .06)
+ self.handrail_height = log_uniform(.02, .06)
+ self.handrail_offset = self.handrail_width * log_uniform(1, 2)
+ self.handrail_extension = uniform(.1, .2)
+ self.handrail_alphas = [self.handrail_offset / self.step_width,
+ 1 - self.handrail_offset / self.step_width]
+ probs = np.array([3, 2, 3])
+ self.handrail_surface = np.random.choice([wood, metal, fabrics], p=probs / probs.sum())
+
+ self.post_height = log_uniform(.8, 1.2)
+ self.post_k = int(np.ceil(self.step_width / self.step_length))
+ self.post_width = self.handrail_width * log_uniform(.6, .8)
+ self.post_minor_width = self.post_width * log_uniform(.3, .5)
+ self.is_post_circular = uniform() < .5
+ probs = np.array([3, 3, 2])
+ self.post_surface = np.random.choice([wood, metal, fabrics], p=probs / probs.sum())
+ self.has_vertical_post = self.handrail_type == 'vertical-post'
+
+ self.has_bars = self.handrail_type == 'horizontal-post'
+ self.bar_size = log_uniform(.1, .2)
+ self.n_bars = int(np.floor(self.post_height / self.bar_size * uniform(.35, .75)))
+
+ self.has_glasses = self.handrail_type == 'glass'
+ self.glass_height = self.post_height - uniform(0, .05)
+ self.glass_margin = self.step_height / 2 + uniform(0, .05)
+ self.glass_surface = glass
+
+ self.has_spiral = False
+ self.mirror = uniform() < .5
+ self.rot_z = np.random.randint(4) * np.pi / 2
+ self.end_margin = self.step_length * 8
+
+ def build_size_config(self):
+ self.n = np.random.randint(13, 21)
+ self.step_height = constants.WALL_HEIGHT / self.n
+ self.step_width = uniform(.8, 1.6)
+ self.step_length = self.step_height * log_uniform(.8, 1.2)
+
+ def make_line(self, alpha):
+ obj = new_line(self.n)
+ x = np.full(self.n + 1, alpha * self.step_width)
+ y = self.step_length * np.arange(self.n + 1)
+ z = self.step_height * np.arange(self.n + 1)
+ np.stack([x, y, z], -1)
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_line_offset(self, alpha):
+ obj = self.make_line(alpha)
+ x, y, z = read_co(obj).T
+ y += self.step_length / 2
+ z += self.step_height
+ z[-1] -= self.step_height
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.n - 1)
+ indices = list(c[0] for c in chunks) + [self.n - 1, self.n]
+ return cos[indices]
+
+ def make_vertical_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.n - 1)
+ indices = sum(list(c[1:].tolist() for c in chunks), []) + [self.n]
+ return cos[indices]
+
+ def split(self, start, end=None):
+ return np.array_split(
+ np.arange(start, end),
+ np.ceil((start if end is None else end - start) / self.post_k)
+ )
+
+ @staticmethod
+ def triangulate(obj):
+ butil.modify_mesh(obj, 'TRIANGULATE', min_vertices=3)
+ levels = 1
+ butil.modify_mesh(obj, 'SUBSURF', levels=levels, render_levels=levels, subdivision_type='SIMPLE')
+ return obj
+
+ def vertical_cut(self, p):
+ cuts = list(LineString([(i, -100), (i, 100)]) for i in range(1, self.n - 1))
+ polygons = cut_polygon_by_line(p, *cuts)
+ parts = []
+ for p in polygons:
+ coords = p.boundary.coords[:][:-1]
+ part = new_circle(vertices=len(coords))
+ with butil.ViewportMode(part, 'EDIT'):
+ bpy.ops.mesh.edge_face_add()
+ write_co(part, np.array(list([0, y * self.step_length, z * self.step_height] for y, z in coords)))
+ parts.append(part)
+ return parts
+
+ def make_steps(self):
+ coords = [(0, 0)]
+ for i in range(self.n):
+ coords.extend([(i, i + 1), (i + 1, i + 1)])
+ coords.extend([(self.n, 0), (0, 0)])
+ p = Polygon(LineString(coords))
+ if self.support_type == 'hole':
+ hole = Polygon(
+ [((1 - self.hole_size) * self.n, 0), (self.n, self.hole_size * self.n), (self.n, 0),
+ ((1 - self.hole_size) * self.n, 0)]
+ )
+ p = p.difference(hole)
+ objs = self.vertical_cut(p)
+ for obj in objs:
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.step_width)
+ self.triangulate(obj)
+ write_attribute(obj, 1, 'steps', 'FACE')
+ return objs
+
+ def make_rails(self):
+ parts = []
+ if self.support_type == 'single-rail':
+ alphas = [.5]
+ else:
+ alphas = [self.rail_offset / self.step_width, 1 - self.rail_offset / self.step_width]
+ for alpha in alphas:
+ obj = self.make_line(alpha)
+ if self.is_rail_circular:
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[self.rail_width, 16])
+ obj.location[-1] = -self.rail_width
+ butil.apply_transform(obj, loc=True)
+ else:
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, -self.rail_height * 2)}
+ )
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.rail_width, offset=0)
+ self.triangulate(obj)
+ write_attribute(obj, 1, 'rails', 'FACE')
+ parts.append(obj)
+ return parts
+
+ def make_treads(self):
+ tread = new_cube(location=(1, 1, 1))
+ butil.apply_transform(tread, loc=True)
+ tread.scale = self.tread_width / 2, self.tread_length / 2, self.tread_height / 2
+ tread.location = -(self.tread_width - self.step_width) / 2, -(
+ self.tread_length - self.step_length), self.step_height
+ butil.apply_transform(tread, loc=True)
+ self.triangulate(tread)
+ write_attribute(tread, 1, 'treads', 'FACE')
+ treads = [tread] + list(butil.deep_clone_obj(tread) for _ in range(self.n - 1))
+ for i in range(1, self.n):
+ treads[i].location = 0, self.step_length * i, self.step_height * i
+ butil.apply_transform(treads[i], loc=True)
+ return treads
+
+ def make_inner_sides(self):
+ offset = -self.side_height / self.step_height
+ if self.side_type == 'zig-zag':
+ coords = [(0, 0)]
+ for i in range(self.n):
+ coords.extend([(i, i + 1), (i + 1, i + 1)])
+ l = LineString(coords)
+ p = l.buffer(offset, join_style='mitre', single_sided=True, )
+ else:
+ p = Polygon(
+ LineString([(0, offset), (0, 1), (self.n, self.n + 1), (self.n, self.n + offset), (0, offset)])
+ )
+ objs = self.vertical_cut(p)
+
+ bottom_cutter = new_cube(location=(0, 0, -1))
+ butil.apply_transform(bottom_cutter, loc=True)
+ bottom_cutter.scale = [100] * 3
+ butil.apply_transform(bottom_cutter)
+ top_cutter = new_cube(location=(0, 0, 1))
+ butil.apply_transform(top_cutter, loc=True)
+ top_cutter.scale = [100] * 3
+ top_cutter.location[-1] = self.n * self.step_height + self.tread_height
+
+ for obj in objs:
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.side_thickness, offset=0)
+ write_attribute(obj, 1, 'sides', 'FACE')
+ for cutter in [top_cutter, bottom_cutter]:
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete([top_cutter, bottom_cutter])
+ return objs
+
+ def make_outer_sides(self):
+ objs = self.make_inner_sides()
+ for obj in objs:
+ obj.location[0] = self.step_width
+ butil.apply_transform(obj, loc=True)
+ return objs
+
+ def make_column(self):
+ return
+
+ def make_handrails(self):
+ parts = []
+ for alpha in self.handrail_alphas:
+ obj = self.make_line_offset(alpha)
+ self.make_single_handrail(obj)
+ parts.append(obj)
+ return parts
+
+ def make_single_handrail(self, obj):
+ self.extend_line(obj, self.handrail_extension)
+ if self.is_handrail_circular:
+ surface.add_geomod(
+ obj, geo_radius, apply=True, input_args=[self.handrail_width, 32],
+ input_kwargs={'to_align_tilt': False}
+ )
+ else:
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, -self.handrail_height * 2)}
+ )
+ butil.modify_mesh(
+ obj, 'SOLIDIFY', thickness=self.handrail_width * 2, offset=0,
+ solidify_mode='NON_MANIFOLD'
+ )
+ butil.modify_mesh(
+ obj, 'BEVEL', width=self.handrail_width * uniform(.2, .5),
+ segments=np.random.randint(4, 7)
+ )
+ obj.location[-1] += self.handrail_height
+ write_attribute(obj, 1, 'handrails', 'FACE')
+ obj.location[-1] += self.post_height
+ butil.apply_transform(obj, loc=True)
+ self.triangulate(obj)
+
+ @staticmethod
+ def extend_line(obj, extension):
+ if len(obj.data.vertices) <= 1:
+ return
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.verts.ensure_lookup_table()
+ v0, v1, v2, v3 = bm.verts[0], bm.verts[1], bm.verts[-1], bm.verts[-2]
+ n_0 = v0.co - v1.co
+ n_0[2] = 0
+ v4 = bm.verts.new(v0.co + n_0 / n_0.length * extension)
+ bm.edges.new((v4, v0))
+ n_1 = v2.co - v3.co
+ n_1[2] = 0
+ v5 = bm.verts.new(v2.co + n_1 / n_1.length * extension)
+ bm.edges.new((v2, v5))
+ bmesh.update_edit_mesh(obj.data)
+
+ def make_posts(self, locs, widths):
+ parts = []
+ existing = np.zeros((0, 3))
+ for loc, width in zip(locs, widths):
+ existing = np.concatenate([existing, loc[:1]], 0)
+ cos = [0]
+ for i, l in enumerate(loc):
+ if i > 0 and np.min(
+ np.linalg.norm(existing - l[np.newaxis, :], axis=1)
+ ) > self.handrail_width * 2:
+ cos.append(i)
+ existing = np.concatenate([existing, loc[i:i + 1]], 0)
+ obj = mesh2obj(data2mesh(loc[cos]))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_vertices_move(TRANSFORM_OT_translate={'value': (0, 0, self.post_height)})
+ if self.is_post_circular:
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[width, 32])
+ else:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (width * 2, 0, 0)})
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={'value': (0, width * 2, 0)})
+ obj.location = -width, -width, 0
+ butil.apply_transform(obj, loc=True)
+ write_attribute(obj, 1, 'posts', 'FACE')
+ parts.append(obj)
+ return parts
+
+ def make_bars(self, locs):
+ parts = []
+ for loc in locs:
+ for loc, loc_ in zip(loc[:-1], loc[1:]):
+ for i in range(self.n_bars):
+ obj = new_line()
+ write_co(obj, np.stack([loc, loc_]))
+ subsurf(obj, 4)
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[self.post_minor_width])
+ obj.location[-1] += self.post_height - (i + 1) * self.bar_size
+ butil.apply_transform(obj, loc=True)
+ write_attribute(obj, 1, 'posts', 'FACE')
+ parts.append(obj)
+ return parts
+
+ def make_glasses(self, locs):
+ parts = []
+ for loc in locs:
+ for loc, loc_ in zip(loc[:-1], loc[1:]):
+ obj = new_line()
+ write_co(obj, np.stack([loc, loc_]))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, self.glass_height - self.glass_margin)}
+ )
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.post_minor_width)
+ obj.location[-1] += self.glass_margin
+ butil.apply_transform(obj, loc=True)
+ write_attribute(obj, 1, 'glasses', 'FACE')
+ parts.append(obj)
+ return parts
+
+ def make_spiral(self, obj):
+ return obj
+
+ def unmake_spiral(self, obj):
+ return obj
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ obj = self.make_line_offset(.5)
+ if self.has_spiral:
+ self.make_spiral(obj)
+ self.extend_line(obj, self.end_margin)
+ self.decorate_line(
+ obj, constants.WALL_THICKNESS / 2,
+ constants.DOOR_SIZE
+ )
+ if self.mirror:
+ mirror(obj)
+ obj.rotation_euler[-1] = self.rot_z
+ butil.apply_transform(obj)
+ return obj
+
+ def create_cutter(self, **kwargs) -> bpy.types.Object:
+ obj = self.make_line_offset(.5)
+ if self.has_spiral:
+ self.make_spiral(obj)
+ self.decorate_line(obj, 0, constants.DOOR_SIZE)
+ if self.mirror:
+ mirror(obj)
+ obj.location[-1] = -constants.WALL_THICKNESS / 2
+ obj.rotation_euler[-1] = self.rot_z
+ butil.apply_transform(obj, True)
+ return obj
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ parts = []
+ if self.has_step:
+ parts.extend(self.make_steps())
+ if self.has_rail:
+ parts.extend(self.make_rails())
+ if self.has_tread:
+ parts.extend(self.make_treads())
+ if self.has_sides:
+ parts.extend(self.make_inner_sides())
+ parts.extend(self.make_outer_sides())
+ parts.extend(self.make_handrails())
+ post_locs = list(self.make_post_locs(alpha) for alpha in self.handrail_alphas)
+ if self.has_vertical_post:
+ vertical_post_locs = list(self.make_vertical_post_locs(alpha) for alpha in self.handrail_alphas)
+ parts.extend(
+ self.make_posts(
+ post_locs + vertical_post_locs,
+ [self.post_width] * len(post_locs) + [self.post_minor_width] * len(
+ vertical_post_locs
+ )
+ )
+ )
+ else:
+ parts.extend(self.make_posts(post_locs, [self.post_width] * len(post_locs)))
+ if self.has_bars:
+ parts.extend(self.make_bars(post_locs))
+ if self.has_glasses:
+ parts.extend(self.make_glasses(post_locs))
+ obj = join_objects(parts)
+ if self.has_spiral:
+ self.make_spiral(obj)
+ if self.has_column:
+ obj = join_objects([obj, self.make_column()])
+ if self.mirror:
+ mirror(obj)
+ obj.rotation_euler[-1] = self.rot_z
+ butil.apply_transform(obj)
+ return obj
+
+ def decorate_line(self, line, low, high):
+ end = np.zeros(len(line.data.vertices))
+ end[[0, -1]] = 1
+ write_attr_data(line, 'end', end)
+ with butil.ViewportMode(line, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={
+ 'value': (0, 0, high - low)
+ }
+ )
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ line.location[-1] -= low
+ butil.modify_mesh(line, 'SOLIDIFY', thickness=self.step_width, offset=0, use_even_offset=True)
+ self.triangulate(line)
+ line.location[-1] -= constants.WALL_THICKNESS / 2
+ butil.apply_transform(line, True)
+ write_attribute(
+ line, lambda nw: nw.compare('LESS_THAN', surface.eval_argument(nw, 'end'), .99),
+ f'staircase_wall', 'FACE', 'INT'
+ )
+ sharp_remesh_with_attrs(line, .05)
+ zeros = np.zeros(len(line.data.polygons), dtype=int)
+ ones = np.ones(len(line.data.polygons), dtype=int)
+ write_attr_data(line, f'{PREFIX}{t.Subpart.Ceiling.value}', zeros, 'INT', 'FACE')
+ write_attr_data(line, f'{PREFIX}{t.Subpart.SupportSurface.value}', zeros, 'INT', 'FACE')
+ write_attr_data(line, f'{PREFIX}{t.Subpart.Wall.value}', ones, 'INT', 'FACE')
+ write_attr_data(line, f'{PREFIX}{t.Subpart.Visible.value}', ones, 'INT', 'FACE')
+ with butil.ViewportMode(line, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+
+ def finalize_assets(self, assets):
+ if self.has_step:
+ self.step_surface.apply(assets, selection='steps', metal_color='bw+natural')
+ if self.has_tread:
+ self.tread_surface.apply(assets, selection='treads', metal_color='bw+natural')
+ if self.has_rail:
+ self.rail_surface.apply(assets, selection='rails')
+ if self.has_sides:
+ self.side_surface.apply(assets, selection='sides')
+ self.handrail_surface.apply(assets, selection='handrails')
+ self.post_surface.apply(assets, selection='posts')
+ if self.has_glasses:
+ self.glass_surface.apply(assets, selection='glasses')
+
+ def make_guardrail(self, mesh):
+ def geo_extrude(nw: NodeWrangler):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ x, y, _ = nw.separate(nw.new_node(Nodes.InputNormal))
+ offset = nw.scale(-self.handrail_offset, nw.vector_math('NORMALIZE', nw.combine(x, y, 0)))
+ geometry = nw.new_node(Nodes.SetPosition, [geometry], input_kwargs={'Offset': offset})
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+ self.unmake_spiral(mesh)
+ with butil.ViewportMode(mesh, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ surface.add_geomod(mesh, geo_extrude, apply=True)
+ remove_faces(mesh, read_attr_data(mesh, 'staircase_wall') == 0)
+ with butil.ViewportMode(mesh, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.select_all(action='INVERT')
+ bpy.ops.mesh.delete(type='EDGE')
+ remove_vertices(
+ mesh, lambda x, y, z: (z < constants.WALL_THICKNESS / 4) | (
+ z > constants.WALL_THICKNESS * 3 / 4)
+ )
+ butil.modify_mesh(mesh, 'WELD', merge_threshold=constants.WALL_THICKNESS / 4)
+ name = mesh.name
+ mesh = separate_loose(mesh)
+ ls = shapely.force_2d(convert2ls(mesh))
+ butil.delete(mesh)
+ parts, locs, minor_locs = [], [], []
+ line = canonicalize_ls(ls)
+ segments = line.segmentize(self.post_k * self.step_length)
+ locs.append(np.array(shapely.force_3d(segments).coords))
+ line = segments.segmentize(self.step_length)
+ if self.has_vertical_post:
+ minor_locs.append(np.array(shapely.force_3d(line).coords))
+ line = shapely.force_3d(line)
+ o = new_line(len(line.coords) - 1)
+ write_co(o, np.array(line.coords))
+ self.make_single_handrail(o)
+ parts.append(o)
+ parts.extend(
+ self.make_posts(
+ locs + minor_locs,
+ [self.post_width] * len(locs) + [self.post_minor_width] * len(minor_locs)
+ )
+ )
+ if self.has_bars:
+ parts.extend(self.make_bars(locs))
+ if self.has_glasses:
+ parts.extend(self.make_glasses(locs))
+ butil.select_none()
+ obj = join_objects(parts)
+ self.make_spiral(obj)
+ self.handrail_surface.apply(obj, selection='handrails')
+ self.post_surface.apply(obj, selection='posts')
+ if self.has_glasses:
+ self.glass_surface.apply(obj, selection='glasses')
+ obj.name = name
+ return obj
+
+ @property
+ def lower(self):
+ return - np.pi / 2
+
+ @property
+ def upper(self):
+ return np.pi / 2
+
+ def valid_contour(self, offset, contour, doors, lower=True):
+ x, y = offset
+ if len(doors) == 0:
+ return True
+ for door in doors:
+ t = self.lower if lower else self.upper
+ t = (np.pi - t if self.mirror else t) + self.rot_z
+ v = np.array([np.cos(t), np.sin(t)])
+ if normalize(np.array([door.location[0] - x, door.location[1] - y])) @ v >= -.5:
+ return True
+ return False
diff --git a/infinigen/assets/elements/staircases/u_shaped.py b/infinigen/assets/elements/staircases/u_shaped.py
new file mode 100644
index 000000000..7c0aeebf1
--- /dev/null
+++ b/infinigen/assets/elements/staircases/u_shaped.py
@@ -0,0 +1,158 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Karhan Kayan: fix constants
+
+import bpy
+import numpy as np
+
+from infinigen.core.constraints.example_solver.room import constants
+from .straight import StraightStaircaseFactory
+from infinigen.assets.utils.decorate import read_co, write_attribute, write_co
+from infinigen.assets.utils.object import new_cube, new_line
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+import infinigen.core.util.blender as butil
+
+
+class UShapedStaircaseFactory(StraightStaircaseFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(UShapedStaircaseFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.m = self.n // 2
+ self.is_rail_circular = True
+
+ def build_size_config(self):
+ self.n = int(np.random.randint(13, 21) / 2) * 2
+ self.step_height = constants.WALL_HEIGHT / self.n
+ self.step_width = log_uniform(.9, 1.5)
+ self.step_length = self.step_height * log_uniform(1, 1.2)
+
+ def make_line(self, alpha):
+ obj = new_line(self.n + 4)
+ x = np.concatenate(
+ [np.full(self.m + 2, alpha * self.step_width), [0], np.full(self.m + 2, -alpha * self.step_width)]
+ )
+ y = np.concatenate(
+ [np.arange(self.m + 1) * self.step_length,
+ [self.m * self.step_length + alpha * self.step_width] * 3,
+ np.arange(self.m, -1, -1) * self.step_length]
+ )
+ z = np.concatenate(
+ [np.arange(self.m + 1), [self.m] * 3, np.arange(self.m, self.n + 1)]
+ ) * self.step_height
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_line_offset(self, alpha):
+ obj = self.make_line(alpha)
+ co = read_co(obj)
+ co[self.m:self.m + 4] = co[self.m + 1:self.m + 5]
+ x, y, z = co.T
+ y[:self.m] += self.step_length / 2
+ y[self.m + 3] += min(self.step_length / 2, alpha * self.step_width)
+ y[self.m + 4:] -= self.step_length / 2
+ z += self.step_height
+ z[[self.m, self.m + 1, self.m + 2, self.m + 3, - 1]] -= self.step_height
+ write_co(obj, np.stack([x, y, z], -1))
+ return obj
+
+ def make_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.m - 1)
+ chunks_ = self.split(self.m + 3, self.n + 4)
+ mid = [self.m - 1, self.m, self.m + 1, self.m + 2, self.m + 3]
+ indices = list(c[0] for c in chunks) + mid + list(c[0] for c in chunks_) + [self.n + 3, self.n + 4]
+ return cos[indices]
+
+ def make_vertical_post_locs(self, alpha):
+ temp = self.make_line_offset(alpha)
+ cos = read_co(temp)
+ butil.delete(temp)
+ chunks = self.split(self.m - 1)
+ chunks_ = np.array_split(np.arange(self.m + 3, self.n + 4), np.ceil((self.n - self.m) / self.post_k))
+ indices = sum(list(c[1:].tolist() for c in chunks + chunks_), [])
+ indices_ = sum(list(c[1:].tolist() for c in chunks_), [])
+ mid_cos = []
+ mid = [self.m - 1, self.m, self.m + 1, self.m + 2]
+ for m in mid:
+ for r in np.linspace(0, 1, self.post_k + 1 if m >= self.m else self.post_k + 2)[1:-1]:
+ mid_cos.append(r * cos[m] + (1 - r) * cos[m + 1])
+ return np.concatenate([cos[indices], np.stack(mid_cos), cos[indices_]], 0)
+
+ def make_steps(self):
+ objs = super(UShapedStaircaseFactory, self).make_steps()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi
+ obj.location = 0, 2 * self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+ lowest = np.min(read_co(objs[self.m]).T[-1])
+ platform = new_cube(location=(0, 1, 1))
+ butil.apply_transform(platform, loc=True)
+ platform.location = 0, self.step_length * self.m, lowest
+ platform.scale = self.step_width, self.step_width / 2, (self.step_height * self.m - lowest) / 2
+ butil.apply_transform(platform, loc=True)
+ write_attribute(platform, 1, 'steps', 'FACE')
+ return objs + [platform]
+
+ def make_treads(self):
+ objs = super(UShapedStaircaseFactory, self).make_treads()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi
+ obj.location = 0, 2 * self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+ platform = new_cube(location=(0, 1, 1))
+ butil.apply_transform(platform, loc=True)
+ platform.location = 0, self.step_length * self.m, self.step_height * self.m
+ platform.scale = self.step_width, self.step_width / 2, self.tread_height / 2
+ butil.apply_transform(platform, loc=True)
+ write_attribute(platform, 1, 'treads', 'FACE')
+ return objs + [platform]
+
+ def make_inner_sides(self):
+ objs = super(UShapedStaircaseFactory, self).make_inner_sides()
+ for obj in objs[self.m:]:
+ obj.rotation_euler[-1] = np.pi
+ obj.location = 0, 2 * self.m * self.step_length, 0
+ butil.apply_transform(obj, loc=True)
+
+ top_cutter = new_cube(location=(0, 0, 1))
+ butil.apply_transform(top_cutter, loc=True)
+ top_cutter.scale = [100] * 3
+ top_cutter.location[-1] = self.m * self.step_height + self.tread_height
+ for obj in objs[:self.m]:
+ butil.modify_mesh(obj, 'BOOLEAN', object=top_cutter, operation='DIFFERENCE')
+ butil.delete(top_cutter)
+ return objs
+
+ def make_outer_sides(self):
+ objs = self.make_inner_sides()
+ for obj in objs[:self.m]:
+ obj.location[0] += self.step_width
+ butil.apply_transform(obj, loc=True)
+ for obj in objs[self.m:]:
+ obj.location[0] -= self.step_width
+ butil.apply_transform(obj, loc=True)
+ platform = new_line(4)
+ x = self.step_width, self.step_width, 0, -self.step_width, -self.step_width
+ mid = self.m * self.step_length + self.step_width
+ y = self.m * self.step_length, mid, mid, mid, self.m * self.step_length
+ z = [self.m * self.step_height] * 5
+ write_co(platform, np.stack([x, y, z], -1))
+ butil.select_none()
+ with butil.ViewportMode(platform, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, -self.side_height)})
+ butil.modify_mesh(platform, 'SOLIDIFY', thickness=self.side_thickness)
+ write_attribute(platform, 1, 'sides', 'FACE')
+ return objs + [platform]
+
+ @property
+ def upper(self):
+ return -np.pi / 2
diff --git a/infinigen/assets/elements/warehouses/__init__.py b/infinigen/assets/elements/warehouses/__init__.py
new file mode 100644
index 000000000..ad3fe7df2
--- /dev/null
+++ b/infinigen/assets/elements/warehouses/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .rack import RackFactory
+from .pallet import PalletFactory
diff --git a/infinigen/assets/elements/warehouses/pallet.py b/infinigen/assets/elements/warehouses/pallet.py
new file mode 100644
index 000000000..754ab60d1
--- /dev/null
+++ b/infinigen/assets/elements/warehouses/pallet.py
@@ -0,0 +1,87 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import wood
+from infinigen.assets.utils.decorate import read_normal
+from infinigen.assets.utils.object import join_objects, new_bbox, new_cube
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import write_attr_data
+from infinigen.core.tagging import PREFIX
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core import tagging, tags as t
+
+
+class PalletFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(PalletFactory, self).__init__(factory_seed, coarse)
+ self.depth = uniform(1.2, 1.4)
+ self.width = uniform(1.2, 1.4)
+ self.thickness = uniform(.01, .015)
+ self.tile_width = uniform(.06, .1)
+ self.tile_slackness = uniform(1.5, 2)
+ self.height = uniform(.2, .25)
+ self.surface = wood
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ bbox = new_bbox(0, self.width, 0, self.depth, 0, self.height)
+ write_attr_data(bbox, f'{PREFIX}{t.Subpart.SupportSurface.value}', read_normal(bbox)[:, -1] > .5, 'INT',
+ 'FACE')
+ return bbox
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ vertical = self.make_vertical()
+ vertical.location[-1] = self.thickness
+ vertical_ = deep_clone_obj(vertical)
+ vertical_.location[-1] = self.height - self.thickness
+ horizontal = self.make_horizontal()
+ horizontal_ = deep_clone_obj(horizontal)
+ horizontal_.location[-1] = self.height - 2 * self.thickness
+ support = self.make_support()
+ support.location[-1] = 2 * self.thickness
+ obj = join_objects([horizontal, horizontal_, vertical, vertical_, support])
+ return obj
+
+ def make_vertical(self):
+ obj = new_cube()
+ obj.location = 1, 1, 1
+ butil.apply_transform(obj, True)
+ obj.scale = self.tile_width / 2, self.depth / 2, self.thickness / 2
+ butil.apply_transform(obj)
+ count = int(np.floor((self.width - self.tile_width) / self.tile_width / self.tile_slackness) / 2) * 2
+ butil.modify_mesh(obj, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ constant_offset_displace=((self.width - self.tile_width) / count, 0, 0),
+ count=count + 1)
+ return obj
+
+ def make_horizontal(self):
+ obj = new_cube()
+ obj.location = 1, 1, 1
+ butil.apply_transform(obj, True)
+ obj.scale = self.width / 2, self.tile_width / 2, self.thickness / 2
+ butil.apply_transform(obj)
+ count = int(np.floor((self.depth - self.tile_width) / self.tile_width / self.tile_slackness) / 2) * 2
+ butil.modify_mesh(obj, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ constant_offset_displace=(0, (self.depth - self.tile_width) / count, 0),
+ count=count + 1)
+ return obj
+
+ def make_support(self):
+ obj = new_cube()
+ obj.location = 1, 1, 1
+ butil.apply_transform(obj, True)
+ obj.scale = self.tile_width / 2, self.tile_width / 2, self.height / 2 - 2 * self.thickness
+ butil.apply_transform(obj)
+ butil.modify_mesh(obj, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ constant_offset_displace=((self.width - self.tile_width) / 2, 0, 0), count=3)
+ butil.modify_mesh(obj, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ constant_offset_displace=(0, (self.depth - self.tile_width) / 2, 0), count=3)
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/elements/warehouses/rack.py b/infinigen/assets/elements/warehouses/rack.py
new file mode 100644
index 000000000..7a7dd2023
--- /dev/null
+++ b/infinigen/assets/elements/warehouses/rack.py
@@ -0,0 +1,168 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.elements.warehouses.pallet import PalletFactory
+from infinigen.assets.materials import metal
+from infinigen.assets.materials.metal import galvanized_metal
+from infinigen.assets.utils.decorate import read_co, remove_faces, solidify, write_attribute, write_co
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import (
+ join_objects, new_base_cylinder, new_bbox, new_cube, new_line,
+ new_plane,
+)
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import write_attr_data
+from infinigen.core.tagging import PREFIX
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core import tagging, tags as t
+from infinigen.core.util.math import FixedSeed
+
+
+class RackFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(RackFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.depth = uniform(1, 1.2)
+ self.width = uniform(4., 5.)
+ self.height = uniform(1.6, 1.8)
+ self.steps = np.random.randint(3, 6)
+ self.thickness = uniform(.06, .08)
+ self.hole_radius = self.thickness / 2 * uniform(.5, .6)
+ self.support_angle = uniform(np.pi / 6, np.pi / 4)
+ self.is_support_round = uniform() < .5
+ self.frame_height = self.thickness * uniform(3, 4)
+ self.frame_count = np.random.randint(20, 30)
+
+ self.stand_surface = self.support_surface = self.frame_surface = metal
+ self.pallet_factory = PalletFactory(self.factory_seed)
+ self.margin_range = .3, .5
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ bbox = new_bbox(
+ -self.depth - self.thickness / 2, self.thickness / 2, -self.thickness / 2, self.width + self.thickness / 2,
+ 0, self.height * self.steps
+ )
+ objs = [bbox]
+ for i in range(self.steps):
+ obj = new_plane()
+ obj.scale = self.depth / 2, self.width / 2 - self.thickness, 1
+ obj.location = -self.depth / 2, self.width / 2, self.height * i
+ butil.apply_transform(obj, True)
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.SupportSurface.value}', np.ones(1).astype(bool), 'INT', 'FACE')
+ objs.append(obj)
+ obj = join_objects(objs)
+ return obj
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ stands = self.make_stands()
+ supports = self.make_supports()
+ frames = self.make_frames()
+ obj = join_objects(stands + supports + frames)
+ co = read_co(obj)
+ co[:, -1] = np.clip(co[:, -1], 0, self.height * self.steps)
+ write_co(obj, co)
+ pallets = [self.pallet_factory(i) for i in range(self.steps * 2)]
+ for i, p in enumerate(pallets):
+ p.parent = obj
+ margin = uniform(*self.margin_range)
+ p.location = margin if i % 2 else self.width - margin - p.dimensions[0], (self.depth - p.dimensions[
+ 1]) / 2, i // 2 * self.height
+ self.pallet_factory.finalize_assets(pallets)
+ for p in pallets:
+ p.parent = obj
+ # obj = join_objects([obj] + pallets)
+ obj.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def make_stands(self):
+ obj = new_cube()
+ obj.scale = [self.thickness / 2] * 3
+ butil.apply_transform(obj, True)
+ cylinder = new_base_cylinder()
+ cylinder.scale = self.hole_radius, self.hole_radius, self.thickness * 2
+ cylinder.rotation_euler[1] = np.pi / 2
+ butil.apply_transform(cylinder)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cylinder, operation='DIFFERENCE')
+ cylinder.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(cylinder)
+ butil.modify_mesh(obj, 'BOOLEAN', object=cylinder, operation='DIFFERENCE')
+ butil.delete(cylinder)
+ remove_faces(
+ obj,
+ lambda x, y, z: (np.abs(x) < self.thickness * .49) & (np.abs(y) < self.thickness * .49) & (
+ np.abs(z) < self.thickness * .49)
+ )
+ remove_faces(obj, lambda x, y, z: np.abs(x) + np.abs(y) < self.thickness * .1)
+ obj.location[-1] = self.thickness / 2
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(
+ obj, 'ARRAY', count=int(np.ceil(self.height / self.thickness * self.steps)),
+ relative_offset_displace=(0, 0, 1), use_merge_vertices=True
+ )
+ write_attribute(obj, 1, 'stand', 'FACE')
+ stands = [obj]
+ for locs in [(0, 1), (1, 1), (1, 0)]:
+ o = deep_clone_obj(obj)
+ o.location = locs[0] * self.width, locs[1] * self.depth, 0
+ butil.apply_transform(o, True)
+ stands.append(o)
+ return stands
+
+ def make_supports(self):
+ n = int(np.floor(self.height * self.steps / self.depth / np.tan(self.support_angle)))
+ obj = new_line(n, self.height * self.steps)
+ obj.rotation_euler[1] = -np.pi / 2
+ butil.apply_transform(obj, True)
+ co = read_co(obj)
+ co[1::2, 1] = self.depth
+ write_co(obj, co)
+ if self.is_support_round:
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[self.thickness / 2, 16])
+ else:
+ solidify(obj, 1, self.thickness)
+ write_attribute(obj, 1, 'support', 'FACE')
+ o = deep_clone_obj(obj)
+ o.location[0] = self.width
+ return [obj, o]
+
+ def make_frames(self):
+ x_bar = new_cube()
+ x_bar.scale = self.width / 2, self.thickness / 2, self.frame_height / 2
+ x_bar.location = self.width / 2, 0, self.height - self.frame_height / 2
+ butil.apply_transform(x_bar, True)
+ x_bar_ = deep_clone_obj(x_bar)
+ x_bar_.location[1] = self.depth
+ butil.apply_transform(x_bar_, True)
+ y_bar = new_cube()
+ y_bar.scale = self.thickness / 2, self.depth / 2, self.thickness / 2
+ margin = self.width / self.frame_count
+ y_bar.location = margin, self.depth / 2, self.height - self.thickness / 2
+ butil.apply_transform(y_bar, True)
+ butil.modify_mesh(
+ y_bar, 'ARRAY', use_relative_offset=False, use_constant_offset=True,
+ count=self.frame_count - 1, constant_offset_displace=(margin, 0, 0)
+ )
+ frames = [x_bar, x_bar_, y_bar]
+ for i in range(1, self.steps - 1):
+ for obj in [x_bar, x_bar_, y_bar]:
+ o = deep_clone_obj(obj)
+ o.location[-1] += self.height * i
+ butil.apply_transform(o, True)
+ frames.append(o)
+
+ for o in frames:
+ write_attribute(o, 1, 'frame', 'FACE')
+ return frames
+
+ def finalize_assets(self, assets):
+ self.stand_surface.apply(assets, 'stand', metal_color='bw')
+ self.support_surface.apply(assets, 'support', metal_color='bw')
+ self.frame_surface.apply(assets, 'frame', metal_color='bw')
diff --git a/infinigen/assets/fluid/__init__.py b/infinigen/assets/fluid/__init__.py
index b5c49b424..1729ae108 100644
--- a/infinigen/assets/fluid/__init__.py
+++ b/infinigen/assets/fluid/__init__.py
@@ -7,4 +7,10 @@
CachedCactusFactory,
CachedCreatureFactory,
CachedTreeFactory
+)
+from .flip_fluid import (
+ make_river,
+ make_still_water,
+ make_tilted_river,
+ make_beach
)
\ No newline at end of file
diff --git a/infinigen/assets/fluid/cached_factory_wrappers.py b/infinigen/assets/fluid/cached_factory_wrappers.py
index 407535cd4..78069b534 100644
--- a/infinigen/assets/fluid/cached_factory_wrappers.py
+++ b/infinigen/assets/fluid/cached_factory_wrappers.py
@@ -1,3 +1,7 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
from infinigen.assets.trees import TreeFactory, BushFactory
from infinigen.assets.creatures import CarnivoreFactory
diff --git a/infinigen/assets/fluid/fluid_scenecomp_additions.py b/infinigen/assets/fluid/fluid_scenecomp_additions.py
index 525bea209..abc713ecf 100644
--- a/infinigen/assets/fluid/fluid_scenecomp_additions.py
+++ b/infinigen/assets/fluid/fluid_scenecomp_additions.py
@@ -1,3 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+
import bpy
import mathutils
from mathutils import Vector
diff --git a/infinigen/assets/fluid/run_asset_cache.py b/infinigen/assets/fluid/run_asset_cache.py
index d921dca56..48cacac68 100644
--- a/infinigen/assets/fluid/run_asset_cache.py
+++ b/infinigen/assets/fluid/run_asset_cache.py
@@ -1,3 +1,8 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
import time
import argparse
import numpy as np
@@ -26,7 +31,7 @@
parser.add_argument("--dom_scale", type=float, default=1)
args = init.parse_args_blender(parser)
- init.apply_gin_configs(configs=[], overrides=[], configs_folder='infinigen_examples/configs')
+ init.apply_gin_configs(configs=[], overrides=[], configs_folder='infinigen_examples/configs_nature')
surface.registry.initialize_from_gin()
factory_name = args.asset
diff --git a/infinigen/assets/fruits/general_fruit.py b/infinigen/assets/fruits/general_fruit.py
index eac821080..ba68eaf72 100644
--- a/infinigen/assets/fruits/general_fruit.py
+++ b/infinigen/assets/fruits/general_fruit.py
@@ -29,7 +29,7 @@
from infinigen.assets.fruits.surfaces.coconuthairy_surface import nodegroup_coconuthairy_surface
from infinigen.assets.fruits.surfaces.coconutgreen_surface import nodegroup_coconutgreen_surface
from infinigen.assets.fruits.surfaces.durian_surface import nodegroup_durian_surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
crosssectionlib = {
'circle_cross_section': nodegroup_circle_cross_section,
diff --git a/infinigen/assets/grassland/dandelion.py b/infinigen/assets/grassland/dandelion.py
index db29e4ec7..51a792af5 100644
--- a/infinigen/assets/grassland/dandelion.py
+++ b/infinigen/assets/grassland/dandelion.py
@@ -16,7 +16,7 @@
from infinigen.core.placement.factory import AssetFactory
import numpy as np
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_pedal_stem_head_geometry', singleton=False, type='GeometryNodeTree')
diff --git a/infinigen/assets/grassland/flower.py b/infinigen/assets/grassland/flower.py
index 348c5829f..711538326 100644
--- a/infinigen/assets/grassland/flower.py
+++ b/infinigen/assets/grassland/flower.py
@@ -17,7 +17,7 @@
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil, color
from infinigen.core.util.math import FixedSeed, dict_lerp
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_polar_to_cart_old', singleton=True)
def nodegroup_polar_to_cart_old(nw):
diff --git a/infinigen/assets/grassland/flowerplant.py b/infinigen/assets/grassland/flowerplant.py
index 792079f26..f09757742 100644
--- a/infinigen/assets/grassland/flowerplant.py
+++ b/infinigen/assets/grassland/flowerplant.py
@@ -18,7 +18,7 @@
from infinigen.assets.grassland import flower as Flower
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_stem_branch_leaf_s_r', singleton=False, type='GeometryNodeTree')
diff --git a/infinigen/assets/grassland/grass_tuft.py b/infinigen/assets/grassland/grass_tuft.py
index 6f5c9dc24..9b61fb06b 100644
--- a/infinigen/assets/grassland/grass_tuft.py
+++ b/infinigen/assets/grassland/grass_tuft.py
@@ -17,7 +17,7 @@
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class GrassTuftFactory(AssetFactory):
diff --git a/infinigen/assets/leaves/leaf.py b/infinigen/assets/leaves/leaf.py
index 6d67c7375..0caaedd47 100644
--- a/infinigen/assets/leaves/leaf.py
+++ b/infinigen/assets/leaves/leaf.py
@@ -15,7 +15,7 @@
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
C = bpy.context
D = bpy.data
diff --git a/infinigen/assets/leaves/leaf_broadleaf.py b/infinigen/assets/leaves/leaf_broadleaf.py
index 5be9cb108..3d69441a0 100644
--- a/infinigen/assets/leaves/leaf_broadleaf.py
+++ b/infinigen/assets/leaves/leaf_broadleaf.py
@@ -18,7 +18,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_random_mask_vein', singleton=False, type='GeometryNodeTree')
diff --git a/infinigen/assets/leaves/leaf_ginko.py b/infinigen/assets/leaves/leaf_ginko.py
index 6c440f5d2..7beee01a9 100644
--- a/infinigen/assets/leaves/leaf_ginko.py
+++ b/infinigen/assets/leaves/leaf_ginko.py
@@ -18,7 +18,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def deg2rad(deg):
return deg / 180.0 * np.pi
diff --git a/infinigen/assets/leaves/leaf_maple.py b/infinigen/assets/leaves/leaf_maple.py
index febe71b86..d28459f4d 100644
--- a/infinigen/assets/leaves/leaf_maple.py
+++ b/infinigen/assets/leaves/leaf_maple.py
@@ -18,7 +18,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def deg2rad(deg):
return deg / 180.0 * np.pi
diff --git a/infinigen/assets/leaves/leaf_pine.py b/infinigen/assets/leaves/leaf_pine.py
index 3ef01abd8..24d25b8c7 100644
--- a/infinigen/assets/leaves/leaf_pine.py
+++ b/infinigen/assets/leaves/leaf_pine.py
@@ -16,7 +16,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
######## code for creating pine needles ########
diff --git a/infinigen/assets/leaves/leaf_v2.py b/infinigen/assets/leaves/leaf_v2.py
index 78cb7eae0..394a89e2c 100644
--- a/infinigen/assets/leaves/leaf_v2.py
+++ b/infinigen/assets/leaves/leaf_v2.py
@@ -29,7 +29,7 @@
from infinigen.core.nodes import node_utils
from infinigen.core.util.color import color_category
from infinigen.core import surface
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('shader_nodegroup_sub_vein', singleton=False, type='ShaderNodeTree')
def shader_nodegroup_sub_vein(nw):
diff --git a/infinigen/assets/lighting/__init__.py b/infinigen/assets/lighting/__init__.py
index a4e8fefe5..daa84a74e 100644
--- a/infinigen/assets/lighting/__init__.py
+++ b/infinigen/assets/lighting/__init__.py
@@ -1,2 +1,11 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
from . import sky_lighting
-from .caustics_lamp import CausticsLampFactory
\ No newline at end of file
+from .caustics_lamp import CausticsLampFactory
+from .ceiling_lights import CeilingLightFactory
+from .ceiling_classic_lamp import CeilingClassicLampFactory
+from .indoor_lights import PointLampFactory
+from .lamp import LampFactory, DeskLampFactory, FloorLampFactory
diff --git a/infinigen/assets/lighting/caustics_lamp.py b/infinigen/assets/lighting/caustics_lamp.py
index 3533c7fe1..fcb374a98 100644
--- a/infinigen/assets/lighting/caustics_lamp.py
+++ b/infinigen/assets/lighting/caustics_lamp.py
@@ -11,7 +11,7 @@
from numpy.random import uniform as U, normal as N, randint, uniform
import numpy as np
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
from infinigen.core.nodes import node_utils
from infinigen.core.placement import placement
diff --git a/infinigen/assets/lighting/ceiling_classic_lamp.py b/infinigen/assets/lighting/ceiling_classic_lamp.py
new file mode 100644
index 000000000..60307e7be
--- /dev/null
+++ b/infinigen/assets/lighting/ceiling_classic_lamp.py
@@ -0,0 +1,239 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Stamatis Alexandropoulos
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging
+from .indoor_lights import PointLampFactory
+from infinigen.assets.utils.autobevel import BevelSharp
+from infinigen.core.util.color import color_category
+
+from infinigen.assets.materials.ceiling_light_shaders import shader_lamp_bulb_nonemissive
+
+
+def shader_lamp_material(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = color_category('textile')
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': rgb, 'Subsurface Radius': (0.1000, 0.1000, 0.1000), 'Roughness': uniform(0.2,0.9), 'Sheen': 0.2068, 'Clearcoat Roughness': 0.1436, 'Transmission': 0.4045, 'Transmission Roughness': 0.6932, 'Emission': (0.9858, 0.9858, 0.9858, 1.0000), 'Emission Strength': 0.0000, 'Alpha': 0.8614})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Scale': 104.3000, 'Randomness': 0.0000},
+ attrs={'feature': 'SMOOTH_F1'})
+
+ displacement = nw.new_node(Nodes.Displacement, input_kwargs={'Height': voronoi_texture.outputs["Distance"], 'Scale': 0.4000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+def shader_inside_medal(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': (0.0018, 0.0015, 0.0000, 1.0000), 'Metallic': 1.0000, 'Roughness': 0.0682})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+def shader_cable(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': (0.0000, 0.0000, 0.0000, 1.0000), 'Metallic': 1.0000, 'Roughness': 0.4273})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('geometry_nodes', singleton=True, type='GeometryNodeTree')
+def geometry_nodes(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'cable_length', 0.7000),
+ ('NodeSocketFloat', 'cable_radius', 0.0500),
+ ('NodeSocketFloat', 'height', 0.0000),
+ ('NodeSocketFloat', 'bottom_radius', 0.0000),
+ ('NodeSocketFloat', 'top_radius', 0.0000),
+ ('NodeSocketFloat', 'Thickness', 0.5000),
+ ('NodeSocketFloatDistance', 'Amount', 1.0000)])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["cable_length"]})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 87, 'Radius': group_input.outputs["cable_radius"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle.outputs["Curve"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh, 'Scale': (1.0000, 1.0000, -1.0000)})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_geometry, 'Material': surface.shaderfunc_to_material(shader_cable)})
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["top_radius"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_geometry_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_3.outputs["Curve"], 'Translation': combine_xyz_4})
+
+ curve_line_3 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (-1.0000, 0.0000, 0.0000), 'End': (1.0000, 0.0000, 0.0000)})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': curve_line_3})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Amount"]})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': reroute},
+ attrs={'domain': 'INSTANCE'})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"]})
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: 1.0000, 1: reroute}, attrs={'operation': 'DIVIDE'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': transform_geometry_4, 'Factor': multiply_1},
+ attrs={'use_all_curves': True})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': realize_instances_1, 'Selection': endpoint_selection_1, 'Position': sample_curve.outputs["Position"]})
+
+ endpoint_selection_2 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Thickness"], 2: 0.0000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ curve_circle_4 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': multiply_add})
+
+ transform_geometry_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_circle_4.outputs["Curve"]})
+
+ sample_curve_1 = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': transform_geometry_5, 'Factor': multiply_1},
+ attrs={'use_all_curves': True})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position, 'Selection': endpoint_selection_2, 'Position': sample_curve_1.outputs["Position"]})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_geometry_4, set_position_1, transform_geometry_5]})
+
+ curve_circle_5 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Thickness"]})
+
+ curve_to_mesh_3 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': join_geometry_3, 'Profile Curve': curve_circle_5.outputs["Curve"], 'Fill Caps': True})
+
+ transform_geometry_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh_3})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_geometry_6, 'Material': surface.shaderfunc_to_material(shader_inside_medal)})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: -1.5000, 1: -0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_2})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: multiply_3}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_4})
+
+ curve_line_2 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_1, 'End': combine_xyz_2})
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': spline_parameter.outputs["Factor"], 3: group_input.outputs["bottom_radius"], 4: group_input.outputs["top_radius"]})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': curve_line_2, 'Radius': map_range.outputs["Result"]})
+
+ curve_circle_2 = nw.new_node(Nodes.CurveCircle)
+
+ curve_to_mesh_2 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': curve_circle_2.outputs["Curve"]})
+
+ flip_faces = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': curve_to_mesh_2})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': curve_to_mesh_2, 'Offset Scale': 0.0050, 'Individual': False})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [flip_faces, extrude_mesh.outputs["Mesh"]]})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_geometry_2, 'Material': surface.shaderfunc_to_material(shader_lamp_material)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material_1, set_material_2]})
+
+ ico_sphere = nw.new_node(Nodes.MeshIcoSphere, input_kwargs={'Radius': 0.0500, 'Subdivisions': 4})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': ico_sphere.outputs["Mesh"], 'Material': surface.shaderfunc_to_material(shader_lamp_bulb_nonemissive)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, join_geometry_1, set_material_3]})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_2, 'Rotation': (0.0000, 3.1416, 0.0000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry_3}, attrs={'is_active_output': True})
+
+
+
+
+
+class CeilingClassicLampFactory(AssetFactory):
+ def __init__(self, factory_seed):
+ super(CeilingClassicLampFactory, self).__init__(factory_seed)
+ with FixedSeed(factory_seed):
+ self.params = {
+ 'cable_length': uniform(0.6, 0.710),
+ 'cable_radius': uniform(0.015,0.02),
+ 'height':uniform(0.4, 0.710),
+ 'top_radius':uniform(0.05, 0.2),
+ 'bottom_radius': uniform(0.22,0.35),
+ 'Thickness': uniform(0.002, 0.006),
+ 'Amount': randint(1, 8)
+ }
+ self.light_factory = PointLampFactory(factory_seed)
+
+ # self.beveler = BevelSharp(mult=uniform(1, 3))
+ def create_placeholder(self, **_):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(
+ obj,
+ 'NODES',
+ node_group=geometry_nodes(),
+ ng_inputs=self.params,
+ apply=True
+ )
+ tagging.tag_system.relabel_obj(obj)
+ return obj
+
+ def create_asset(self, i, placeholder, face_size, **_):
+ obj = butil.deep_clone_obj(placeholder, keep_materials=True)
+ light = self.light_factory.spawn_asset(i)
+ butil.parent_to(light, obj)
+ return obj
diff --git a/infinigen/assets/lighting/ceiling_lights.py b/infinigen/assets/lighting/ceiling_lights.py
new file mode 100644
index 000000000..ccf31fa34
--- /dev/null
+++ b/infinigen/assets/lighting/ceiling_lights.py
@@ -0,0 +1,217 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# -
+# - Alexander Raistrick: add point light
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed, clip_gaussian
+from infinigen.core.placement.factory import AssetFactory
+
+from .indoor_lights import PointLampFactory
+from infinigen.assets.utils.autobevel import BevelSharp
+from infinigen.assets.material_assignments import AssetList
+
+
+class CeilingLightFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]):
+ super(CeilingLightFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ self.ceiling_light_default_params = [{
+ "Radius": 0.2,
+ "Thickness": 0.001,
+ "InnerRadius": 0.2,
+ "Height": 0.1,
+ "InnerHeight": 0.1,
+ "Curvature": 0.1,
+ }, {
+ "Radius": 0.18,
+ "Thickness": 0.05,
+ "InnerRadius": 0.18,
+ "Height": 0.1,
+ "InnerHeight": 0.1,
+ "Curvature": 0.25,
+ }, {
+ "Radius": 0.2,
+ "Thickness": 0.005,
+ "InnerRadius": 0.18,
+ "Height": 0.1,
+ "InnerHeight": 0.03,
+ "Curvature": 0.4,
+ }]
+ with FixedSeed(factory_seed):
+ self.light_factory = PointLampFactory(factory_seed)
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+
+ self.params.update(self.material_params)
+ self.beveler = BevelSharp(mult=U(1, 3))
+
+ def get_material_params(self):
+ material_assignments = AssetList['CeilingLightFactory']()
+ black_material = material_assignments['black_material'].assign_material()
+ white_material = material_assignments['white_material'].assign_material()
+
+ wrapped_params = {
+ 'BlackMaterial': surface.shaderfunc_to_material(black_material),
+ 'WhiteMaterial': surface.shaderfunc_to_material(white_material),
+ }
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+
+ def sample_parameters(self, dimensions, use_default=False):
+ if use_default:
+ return self.ceiling_light_default_params[RI(0, len(self.ceiling_light_default_params))]
+ else:
+ Radius = clip_gaussian(0.12, 0.04, 0.1, 0.25)
+ Thickness = U(0.005, 0.05)
+ InnerRadius = Radius * U(0.4, 0.9)
+ Height = 0.7 * clip_gaussian(0.09, 0.03, 0.07, 0.15)
+ InnerHeight = Height * U(0.5, 1.1)
+ Curvature = U(0.1, 0.5)
+ params = {
+ "Radius": Radius,
+ "Thickness": Thickness,
+ "InnerRadius": InnerRadius,
+ "Height": Height,
+ "InnerHeight": InnerHeight,
+ "Curvature": Curvature,
+ }
+ return params
+
+ def create_placeholder(self, i, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_ceiling_light_geometry(), ng_inputs=self.params, apply=True)
+ return obj
+
+ def create_asset(self, i, placeholder, **params):
+ obj = butil.copy(placeholder, keep_materials=True)
+ self.beveler(obj)
+
+ lamp = self.light_factory.spawn_asset(i, loc=(0,0,0), rot=(0,0,0))
+
+ butil.parent_to(lamp, obj, no_transform=True, no_inverse=True)
+ lamp.location.z -= 0.03
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+@node_utils.to_nodegroup('nodegroup_ceiling_light_geometry', singleton=True, type='GeometryNodeTree')
+def nodegroup_ceiling_light_geometry(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 0.2000),
+ ('NodeSocketFloat', 'Thickness', 0.0050),
+ ('NodeSocketFloat', 'InnerRadius', 0.1800),
+ ('NodeSocketFloat', 'Height', 0.1000),
+ ('NodeSocketFloat', 'InnerHeight', 0.0300),
+ ('NodeSocketFloat', 'Curvature', 0.4000),
+ ('NodeSocketMaterial', 'BlackMaterial', None),
+ ('NodeSocketMaterial', 'WhiteMaterial', None)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 512, 'Radius': group_input.outputs["Radius"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle.outputs["Curve"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh,
+ input_kwargs={'Mesh': curve_to_mesh, 'Offset Scale': group_input.outputs["Thickness"], 'Individual': False})
+
+ flip_faces = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': curve_to_mesh})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [extrude_mesh.outputs["Mesh"], flip_faces]})
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': join_geometry, 'Shade Smooth': False})
+
+ mesh_circle = nw.new_node(Nodes.MeshCircle, input_kwargs={'Radius': group_input.outputs["Radius"]}, attrs={'fill_type': 'NGON'})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_shade_smooth, mesh_circle]})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_1, 'Material': group_input.outputs["BlackMaterial"]})
+
+ ico_sphere_1 = nw.new_node(Nodes.MeshIcoSphere, input_kwargs={'Radius': group_input.outputs["InnerRadius"], 'Subdivisions': 5})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': ico_sphere_1.outputs["Mesh"], 'Name': 'UVMap', 3: ico_sphere_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ position_2 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_2})
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_2.outputs["Z"], 1: 0.0010}, attrs={'operation': 'LESS_THAN'})
+
+ separate_geometry_1 = nw.new_node(Nodes.SeparateGeometry, input_kwargs={'Geometry': store_named_attribute, 'Selection': less_than})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["InnerHeight"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': 1.0000, 'Z': group_input.outputs["Curvature"]})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': separate_geometry_1.outputs["Selection"], 'Translation': combine_xyz_2, 'Scale': combine_xyz_3})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (0.0000, 0.0000, -0.0010), 'End': combine_xyz_1})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["InnerRadius"]})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line_1, 'Profile Curve': curve_circle_1.outputs["Curve"], 'Fill Caps': True})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, curve_to_mesh_1]})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_2, 'Material': group_input.outputs["WhiteMaterial"]})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': join_geometry_3})
+
+ vector = nw.new_node(Nodes.Vector)
+ vector.vector = (0.0000, 0.0000, 0.0000)
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': join_geometry_3, 'Bounding Box': bounding_box.outputs["Bounding Box"], 'LightPosition': vector},
+ attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/lighting/hdri_lighting.py b/infinigen/assets/lighting/hdri_lighting.py
new file mode 100644
index 000000000..89aad92b6
--- /dev/null
+++ b/infinigen/assets/lighting/hdri_lighting.py
@@ -0,0 +1,33 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Lingjie Mei
+import os
+
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.random import random_general as rg
+
+HDRI_RESOURCES = f"{os.getcwd()}/resources/hdri"
+
+
+@gin.configurable
+def hdri_lighting(nw: NodeWrangler, strength=("uniform", 0.8, 1.2), ):
+ suffixes = [f for f in os.listdir(HDRI_RESOURCES) if f.endswith('.exr')]
+ suffix = np.random.choice(suffixes)
+ image = bpy.data.images.load(filepath=f"{HDRI_RESOURCES}/{suffix}",check_existing=True)
+ texture_coord = nw.new_node(Nodes.TextureCoord)
+ coord = nw.new_node(Nodes.Mapping, [texture_coord], input_kwargs={'Rotation': (0, 0, uniform(np.pi * 2))})
+ texture = nw.new_node(Nodes.EnvironmentTexture, [coord], attrs={'image': image})
+ return nw.new_node(Nodes.Background, input_kwargs={'Color': texture, 'Strength': rg(strength)})
+
+
+def add_lighting():
+ nw = NodeWrangler(bpy.context.scene.world.node_tree)
+ surface = hdri_lighting(nw)
+ nw.new_node(Nodes.WorldOutput, input_kwargs={'Surface': surface})
diff --git a/infinigen/assets/lighting/holdout_lighting.py b/infinigen/assets/lighting/holdout_lighting.py
new file mode 100644
index 000000000..b4d177edf
--- /dev/null
+++ b/infinigen/assets/lighting/holdout_lighting.py
@@ -0,0 +1,34 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Lingjie Mei
+import os
+
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.random import random_general as rg
+
+HOLDOUT_RESOURCES = f"{os.getcwd()}/resources/holdout"
+
+
+@gin.configurable
+def holdout_lighting(nw: NodeWrangler, strength=("uniform", 0.8, 1.2), ):
+ suffixes = [f for f in os.listdir(HOLDOUT_RESOURCES) if f.endswith('.png')]
+ suffix = np.random.choice(suffixes)
+ image = bpy.data.images.load(filepath=f"{HOLDOUT_RESOURCES}/{suffix}",check_existing=True)
+ texture_coord = nw.new_node(Nodes.TextureCoord)
+ coord = nw.new_node(Nodes.Mapping, [texture_coord], input_kwargs={'Rotation': (0, 0, uniform(np.pi * 2))})
+ texture = nw.new_node(Nodes.EnvironmentTexture, [coord], attrs={'image': image})
+ return nw.new_node(Nodes.Background, input_kwargs={'Color': texture, 'Strength': rg(strength)})
+
+
+def add_lighting():
+ nw = NodeWrangler(bpy.context.scene.world.node_tree)
+ surface = holdout_lighting(nw)
+ nw.new_node(Nodes.WorldOutput, input_kwargs={'Surface': surface})
+ bpy.context.scene.world.cycles_visibility.camera = False
diff --git a/infinigen/assets/lighting/indoor_lights.py b/infinigen/assets/lighting/indoor_lights.py
new file mode 100644
index 000000000..a4f02e9bc
--- /dev/null
+++ b/infinigen/assets/lighting/indoor_lights.py
@@ -0,0 +1,56 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+from mathutils import Vector
+
+from numpy.random import uniform as U, normal as N, randint, uniform
+import numpy as np
+
+from infinigen.core.util.random import log_uniform
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.placement import placement
+from infinigen.core.placement.placement import placeholder_locs
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import clip_gaussian
+
+
+def shader_blackbody_temp(nw, params):
+ blackbody = nw.new_node(Nodes.BlackBody, input_kwargs={'Temperature': params['Temperature']})
+ emission = nw.new_node(Nodes.Emission, input_kwargs={'Color': blackbody})
+ nw.new_node(Nodes.LightOutput, [emission])
+
+class PointLampFactory(AssetFactory):
+
+ def __init__(self, factory_seed):
+ super().__init__(factory_seed)
+ with FixedSeed(factory_seed):
+ self.params = {
+ 'Wattage': U(40, 100),
+ 'Radius': U(0.02, 0.03),
+ 'Temperature': clip_gaussian(4700, 700, 3500, 6500)
+ }
+
+ def create_placeholder(self, **_):
+ cube = butil.spawn_cube(size=2)
+ cube.scale = (self.params['Radius'],) * 3
+ butil.apply_transform(cube)
+ return cube
+
+ def create_asset(self, **_) -> bpy.types.Object:
+ bpy.ops.object.light_add(type='POINT')
+ lamp = bpy.context.active_object
+ lamp.data.energy = self.params['Wattage']
+ lamp.data.shadow_soft_size = self.params['Radius']
+ lamp.data.use_nodes = True
+
+ nw = NodeWrangler(lamp.data.node_tree)
+ shader_blackbody_temp(nw, params=self.params)
+
+ return lamp
\ No newline at end of file
diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py
new file mode 100644
index 000000000..f78caded8
--- /dev/null
+++ b/infinigen/assets/lighting/lamp.py
@@ -0,0 +1,548 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Hongyu Wen: primary author
+# - Alexander Raistrick: add point light
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from .indoor_lights import PointLampFactory
+from infinigen.assets.material_assignments import AssetList
+
+class LampFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], lamp_type="FloorLamp"):
+ super(LampFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.bulb_fac = PointLampFactory(factory_seed)
+ self.bulb_fac.params['Temperature'] = max(self.bulb_fac.params['Temperature'] * 0.6, 2500)
+ self.bulb_fac.params['Wattage'] *= 0.5
+
+ self.dimensions = dimensions
+ self.lamp_type = lamp_type
+ self.lamp_default_params = {
+ "DeskLamp":{
+ "StandRadius": 0.01,
+ "StandHeight": 0.3,
+ "BaseRadius": 0.07,
+ "BaseHeight": 0.02,
+ "ShadeHeight": 0.18,
+ "HeadTopRadius": 0.08,
+ "HeadBotRadius": 0.11,
+ "ReverseLamp": True,
+ "RackThickness": 0.002,
+ "CurvePoint1": (0.0, 0.0, 0.0),
+ "CurvePoint2": (0.0, 0.0, 0.2),
+ "CurvePoint3": (0.0, 0.0, 0.3)
+ },
+ "FloorLamp1": {
+ "StandRadius": 0.01,
+ "StandHeight": 0.3,
+ "BaseRadius": 0.1,
+ "BaseHeight": 0.02,
+ "ShadeHeight": 0.2,
+ "HeadTopRadius": 0.1,
+ "HeadBotRadius": 0.12,
+ "ReverseLamp": False,
+ "RackThickness": 0.002,
+ "CurvePoint1": (0.0, 0.0, 1.0),
+ "CurvePoint2": (0.05, 0.0, 1.2),
+ "CurvePoint3": (0.2, 0.0, 1.0)
+ },
+ "FloorLamp2": {
+ "StandRadius": 0.01,
+ "StandHeight": 0.3,
+ "BaseRadius": 0.1,
+ "BaseHeight": 0.02,
+ "ShadeHeight": 0.2,
+ "HeadTopRadius": 0.1,
+ "HeadBotRadius": 0.11,
+ "ReverseLamp": True,
+ "RackThickness": 0.002,
+ "CurvePoint1": (0.0, 0.0, 1.0),
+ "CurvePoint2": (0.0, 0.0, 1.1),
+ "CurvePoint3": (0.0, 0.0, 1.2)
+ }}
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['LampFactory']()
+ black_material = material_assignments['black_material'].assign_material()
+ white_material = material_assignments['metal'].assign_material()
+ lampshade_material = material_assignments['lampshade'].assign_material()
+
+ wrapped_params = {
+ 'BlackMaterial': surface.shaderfunc_to_material(black_material),
+ 'MetalMaterial': surface.shaderfunc_to_material(white_material),
+ 'LampshadeMaterial': surface.shaderfunc_to_material(lampshade_material)
+ }
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ def sample_parameters(self, dimensions, use_default=False):
+ if use_default:
+ if self.lamp_type == "DeskLamp":
+ return self.lamp_default_params["DeskLamp"]
+ else:
+ return random.choice([self.lamp_default_params["FloorLamp1"], self.lamp_default_params["FloorLamp2"]])
+ else:
+ stand_radius = U(0.005, 0.015)
+ base_radius = U(0.05, 0.15)
+ base_height = U(0.01, 0.03)
+ shade_height = U(0.18, 0.3)
+ head_top_radius = U(0.07, 0.15)
+ head_bot_radius = head_top_radius + U(0, 0.05)
+ rack_thickness = U(0.001, 0.003)
+ reverse_lamp = True
+
+ if self.lamp_type == "DeskLamp":
+ height = U(0.25, 0.4)
+ else:
+ height = U(1, 1.5)
+
+ z1 = U(base_height, height)
+ z2 = U(z1, height)
+ z3 = height
+
+ x1, x2, x3 = 0, 0, 0
+ # if self.lamp_type == "FloorLamp" and U() < 0.5:
+ # x2 = U(0.03, 0.1)
+ # x3 = U(0.2, 0.4)
+ # z2, z3 = z3, z2
+ # reverse_lamp = False
+
+ params = {
+ "StandRadius": stand_radius,
+ "BaseRadius": base_radius,
+ "BaseHeight": base_height,
+ "ShadeHeight": shade_height,
+ "HeadTopRadius": head_top_radius,
+ "HeadBotRadius": head_bot_radius,
+ "ReverseLamp": reverse_lamp,
+ "RackThickness": rack_thickness,
+ "CurvePoint1": (x1, 0.0, z1),
+ "CurvePoint2": (x2, 0.0, z2),
+ "CurvePoint3": (x3, 0.0, z3)
+ }
+ return params
+
+ def create_asset(self, i, **params):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_lamp_geometry(), ng_inputs=self.params, apply=True)
+
+ if np.random.uniform() < 0.6:
+ bulb = self.bulb_fac(i)
+ butil.parent_to(bulb, obj, no_inverse=True, no_transform=True)
+ bulb.location.z = obj.bound_box[-2][2] - self.params['ShadeHeight'] * 0.5
+
+ with butil.SelectObjects(obj):
+ bpy.ops.object.shade_flat()
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+class DeskLampFactory(LampFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse=coarse, lamp_type='DeskLamp')
+
+class FloorLampFactory(LampFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse, lamp_type=np.random.choice(['FloorLamp1', 'FloorLamp2']))
+
+
+
+
+@node_utils.to_nodegroup('nodegroup_bulb', singleton=False, type='GeometryNodeTree')
+def nodegroup_bulb(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[
+ ('NodeSocketMaterial', 'LampshadeMaterial', None),
+ ('NodeSocketMaterial', 'MetalMaterial', None)])
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (0.0000, 0.0000, -0.2000), 'End': (0.0000, 0.0000, 0.0000)})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.1500, 'Resolution': 100})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line_1, 'Profile Curve': curve_circle_1.outputs["Curve"], 'Fill Caps': True})
+
+ spiral = nw.new_node('GeometryNodeCurveSpiral',
+ input_kwargs={'Rotations': 5.0000, 'Start Radius': 0.1500, 'End Radius': 0.1500, 'Height': 0.2000})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': spiral, 'Translation': (0.0000, 0.0000, -0.2000)})
+
+ curve_circle_2 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.0150, 'Resolution': 100})
+
+ curve_to_mesh_2 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': transform, 'Profile Curve': curve_circle_2.outputs["Curve"], 'Fill Caps': True})
+
+ curve_line_2 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (0.0000, 0.0000, -0.2000), 'End': (0.0000, 0.0000, -0.3000)})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line_2, 'Count': 100})
+
+ spline_parameter_1 = nw.new_node(Nodes.SplineParameter)
+
+ float_curve_1 = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter_1.outputs["Factor"]})
+ node_utils.assign_curve(float_curve_1.mapping.curves[0], [(0.0000, 1.0000), (0.4432, 0.5500), (1.0000, 0.2750)], handles=['AUTO', 'VECTOR', 'AUTO'])
+
+ set_curve_radius_1 = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': resample_curve_1, 'Radius': float_curve_1})
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.1500, 'Resolution': 100})
+
+ curve_to_mesh_3 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius_1, 'Profile Curve': curve_circle_3.outputs["Curve"], 'Fill Caps': True})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh_1, curve_to_mesh_2, curve_to_mesh_3]})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_1, 'Material': group_input.outputs['MetalMaterial']})
+
+ curve_line = nw.new_node(Nodes.CurveLine)
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line, 'Count': 100})
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], [(0.0000, 0.1500), (0.0500, 0.1700), (0.1500, 0.2000), (0.5500, 0.3800), (0.8000, 0.3500), (0.9568, 0.2200), (1.0000, 0.0000)])
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': resample_curve, 'Radius': float_curve})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 100})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': curve_circle.outputs["Curve"]})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': curve_to_mesh, 'Material': group_input.outputs['LampshadeMaterial']})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': (0.0000, 0.0000, 0.3000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_bulb_rack', singleton=False, type='GeometryNodeTree')
+def nodegroup_bulb_rack(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ amount = nw.new_node(Nodes.GroupInput,
+ label='amount',
+ expose_input=[('NodeSocketFloatDistance', 'Thickness', 0.0200),
+ ('NodeSocketInt', 'Amount', 3),
+ ('NodeSocketFloatDistance', 'InnerRadius', 1.0000),
+ ('NodeSocketFloatDistance', 'OuterRadius', 1.0000),
+ ('NodeSocketFloat', 'InnerHeight', 0.0000),
+ ('NodeSocketFloat', 'OuterHeight', 0.0000)])
+
+ curve_circle_2 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': amount.outputs["OuterRadius"], 'Resolution': 100})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': amount.outputs["OuterHeight"]})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_2.outputs["Curve"], 'Translation': combine_xyz})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (-1.0000, 0.0000, 0.0000), 'End': (1.0000, 0.0000, 0.0000)})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': curve_line})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': amount.outputs["Amount"]})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': reroute},
+ attrs={'domain': 'INSTANCE'})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': duplicate_elements.outputs["Geometry"]})
+
+ endpoint_selection = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: 1.0000, 1: reroute}, attrs={'operation': 'DIVIDE'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': transform, 'Factor': multiply},
+ attrs={'use_all_curves': True})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': realize_instances, 'Selection': endpoint_selection, 'Position': sample_curve.outputs["Position"]})
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: amount.outputs["Thickness"], 2: amount.outputs["InnerRadius"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': multiply_add, 'Resolution': 100})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': amount.outputs["InnerHeight"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Translation': combine_xyz_1})
+
+ sample_curve_1 = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': transform_1, 'Factor': multiply},
+ attrs={'use_all_curves': True})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position, 'Selection': endpoint_selection_1, 'Position': sample_curve_1.outputs["Position"]})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, set_position_1, transform_1]})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': amount.outputs["Thickness"], 'Resolution': 100})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': join_geometry, 'Profile Curve': curve_circle_1.outputs["Curve"], 'Fill Caps': True})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': curve_to_mesh}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_reversiable_bulb', singleton=False, type='GeometryNodeTree')
+def nodegroup_reversiable_bulb(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Scale', 0.3000),
+ ('NodeSocketBool', 'Reverse', False),
+ ('NodeSocketMaterial', 'BlackMaterial', None),
+ ('NodeSocketMaterial', 'LampshadeMaterial', None),
+ ('NodeSocketMaterial', 'MetalMaterial', None)])
+
+ bulb = nw.new_node(nodegroup_bulb().name, input_kwargs={'LampshadeMaterial': group_input.outputs["LampshadeMaterial"],
+ 'MetalMaterial': group_input.outputs["MetalMaterial"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Scale"], 'Y': group_input.outputs["Scale"], 'Z': group_input.outputs["Scale"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': bulb, 'Scale': combine_xyz_1})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': transform})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Reverse"], 1: 3.1415}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances, input_kwargs={'Instances': geometry_to_instance, 'Rotation': combine_xyz_2})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Reverse"], 1: 2.0000, 2: -1.0000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: -0.0150, 1: multiply_add}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': rotate_instances, 'RackSupport': multiply_1},
+ attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_lamp_head', singleton=False, type='GeometryNodeTree')
+def nodegroup_lamp_head(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'ShadeHeight', 0.0000),
+ ('NodeSocketFloat', 'TopRadius', 0.3000),
+ ('NodeSocketFloat', 'BotRadius', 0.5000),
+ ('NodeSocketBool', 'ReverseBulb', True),
+ ('NodeSocketFloatDistance', 'RackThickness', 0.0050),
+ ('NodeSocketFloat', 'RackHeight', 0.5000),
+ ('NodeSocketMaterial', 'BlackMaterial', None),
+ ('NodeSocketMaterial', 'LampshadeMaterial', None),
+ ('NodeSocketMaterial', 'MetalMaterial', None)])
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["TopRadius"], 1: 0.8000},
+ attrs={'operation': 'MULTIPLY'})
+
+ reversiable_bulb = nw.new_node(nodegroup_reversiable_bulb().name,
+ input_kwargs={'Scale': multiply,
+ 'BlackMaterial': group_input.outputs["BlackMaterial"],
+ 'LampshadeMaterial': group_input.outputs["LampshadeMaterial"],
+ 'MetalMaterial': group_input.outputs["MetalMaterial"]})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.1500}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["ReverseBulb"], 1: 2.0000, 2: -1.0000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["RackHeight"], 1: multiply_add},
+ attrs={'operation': 'MULTIPLY'})
+
+ bulb_rack = nw.new_node(nodegroup_bulb_rack().name,
+ input_kwargs={'Thickness': group_input.outputs["RackThickness"], 'InnerRadius': multiply_1, 'OuterRadius': group_input.outputs["TopRadius"], 'InnerHeight': reversiable_bulb.outputs["RackSupport"], 'OuterHeight': multiply_2})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': bulb_rack, 'Material': group_input.outputs["BlackMaterial"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_2})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["ShadeHeight"], 1: group_input.outputs["RackHeight"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: multiply_3}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_4})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_1, 'End': combine_xyz})
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': spline_parameter.outputs["Factor"], 3: group_input.outputs["TopRadius"], 4: group_input.outputs["BotRadius"]})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': curve_line, 'Radius': map_range.outputs["Result"]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 100})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': curve_circle.outputs["Curve"]})
+
+ flip_faces = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': curve_to_mesh})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': curve_to_mesh, 'Offset Scale': 0.0050, 'Individual': False})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [flip_faces, extrude_mesh.outputs["Mesh"]]})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_1, 'Material': group_input.outputs["LampshadeMaterial"]})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [reversiable_bulb.outputs["Geometry"], set_material, set_material_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_lamp_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_lamp_geometry(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'StandRadius', 0.0200),
+ ('NodeSocketFloatDistance', 'BaseRadius', 0.1000),
+ ('NodeSocketFloat', 'BaseHeight', 0.0200),
+ ('NodeSocketFloat', 'ShadeHeight', 0.0000),
+ ('NodeSocketFloat', 'HeadTopRadius', 0.3000),
+ ('NodeSocketFloat', 'HeadBotRadius', 0.5000),
+ ('NodeSocketBool', 'ReverseLamp', True),
+ ('NodeSocketFloatDistance', 'RackThickness', 0.0050),
+ ('NodeSocketVectorTranslation', 'CurvePoint1', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVectorTranslation', 'CurvePoint2', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVectorTranslation', 'CurvePoint3', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketMaterial', 'BlackMaterial', None),
+ ('NodeSocketMaterial', 'LampshadeMaterial', None),
+ ('NodeSocketMaterial', 'MetalMaterial', None)])
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["BaseHeight"]})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_1})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["BaseRadius"], 'Resolution': 100})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line_1, 'Profile Curve': curve_circle_1.outputs["Curve"], 'Fill Caps': True})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["BaseHeight"]})
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Start': combine_xyz, 'Start Handle': group_input.outputs["CurvePoint1"], 'End Handle': group_input.outputs["CurvePoint2"], 'End': group_input.outputs["CurvePoint3"], 'Resolution': 100})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [bezier_segment, curve_line]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["StandRadius"], 'Resolution': 100})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': join_geometry_2, 'Profile Curve': curve_circle.outputs["Curve"], 'Fill Caps': True})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh_1, curve_to_mesh]})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry, 'Material': group_input.outputs["BlackMaterial"]})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["ShadeHeight"], 1: 0.4000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["ShadeHeight"], 1: 0.2000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["ReverseLamp"], 2: multiply_1},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ lamp_head = nw.new_node(nodegroup_lamp_head().name,
+ input_kwargs={'ShadeHeight': group_input.outputs["ShadeHeight"],
+ 'TopRadius': group_input.outputs["HeadTopRadius"],
+ 'BotRadius': group_input.outputs["HeadBotRadius"],
+ 'ReverseBulb': group_input.outputs["ReverseLamp"],
+ 'RackThickness': group_input.outputs["RackThickness"],
+ 'RackHeight': multiply_add,
+ 'BlackMaterial': group_input.outputs["BlackMaterial"],
+ 'LampshadeMaterial': group_input.outputs["LampshadeMaterial"],
+ 'MetalMaterial': group_input.outputs["MetalMaterial"],})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': bezier_segment, 'Factor': 1.0000},
+ attrs={'use_all_curves': True})
+
+ align_euler_to_vector = nw.new_node(Nodes.AlignEulerToVector, input_kwargs={'Vector': sample_curve.outputs["Tangent"]}, attrs={'axis': 'Z'})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': lamp_head, 'Translation': sample_curve.outputs["Position"], 'Rotation': align_euler_to_vector})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, transform]})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': join_geometry_1})
+
+ curve_line_2 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': (0.0000, 0.0000, 0.1000)})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_line_2, 'Translation': sample_curve.outputs["Position"], 'Rotation': align_euler_to_vector})
+
+ sample_curve_1 = nw.new_node(Nodes.SampleCurve, input_kwargs={'Curves': transform_geometry, 'Factor': 1.0000})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': join_geometry_1, 'Bounding Box': bounding_box.outputs["Bounding Box"], 'LightPosition': sample_curve_1.outputs["Position"]},
+ attrs={'is_active_output': True})
+
diff --git a/infinigen/assets/lighting/sky_lighting.py b/infinigen/assets/lighting/sky_lighting.py
index 1249bfb61..4593ef280 100644
--- a/infinigen/assets/lighting/sky_lighting.py
+++ b/infinigen/assets/lighting/sky_lighting.py
@@ -52,6 +52,7 @@ def nishita_lighting(
sky_texture.air_density =rg(air_density)
sky_texture.dust_density = rg(dust_density)
sky_texture.ozone_density = clip_gaussian(1, 1, 0.1, 10)
+
strength = rg(strength)
return nw.new_node(Nodes.Background, input_kwargs={'Color': sky_texture, 'Strength': strength})
diff --git a/infinigen/assets/lighting/three_point_lighting.py b/infinigen/assets/lighting/three_point_lighting.py
new file mode 100644
index 000000000..e1dba2e16
--- /dev/null
+++ b/infinigen/assets/lighting/three_point_lighting.py
@@ -0,0 +1,28 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.object import center
+
+
+def add_lighting(asset):
+ dimension = asset.dimensions * asset.scale
+ radius = np.sqrt(dimension[0] * dimension[1]) / 2 * 1.5
+ locations = np.array(
+ [(uniform(3, 4), -uniform(3, 4), uniform(5, 6)), (uniform(3, 4), uniform(3, 4), uniform(3, 4)),
+ (-uniform(5, 6), uniform(-2, -3), uniform(3, 4))]) * radius
+ energies = [1000, 1000 / uniform(5, 10), 1000 * uniform(5, 10)]
+ for loc, energy in zip(locations, energies):
+ bpy.ops.object.light_add(type='SPOT')
+ light = bpy.context.active_object
+ light.location = loc + asset.location + center(asset) * asset.scale
+ light.rotation_euler = 0, np.arctan2(np.sqrt(loc[0] ** 2 + loc[1] ** 2), loc[2]), -np.arctan2(-loc[0],
+ -loc[
+ 1])\
+ - np.pi / 2
+ light.data.energy = energy * radius * radius
diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py
new file mode 100644
index 000000000..a11d1bdff
--- /dev/null
+++ b/infinigen/assets/material_assignments.py
@@ -0,0 +1,592 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+
+
+import numpy as np
+from dataclasses import dataclass
+import functools
+
+from numpy.random import uniform
+
+from infinigen.assets.materials import (metal, plastic, text, ceramic, woods, dirt,
+ mirror,
+ wood, glass_volume, fabrics, plaster,
+ sofa_fabric, leather, rug, water, glass)
+from infinigen.assets.materials.plastics import plastic_rough
+from infinigen.assets.materials.plastics.plastic_rough import shader_rough_plastic
+from infinigen.assets.materials import (glass_volume, plaster, wood,
+ sofa_fabric, leather, rug, water, glass, velvet)
+from infinigen.assets.materials import (beverage_fridge_shaders, dishwasher_shaders,
+ ceiling_light_shaders,
+ vase_shaders,
+ lamp_shaders, table_marble,
+ fabrics,
+ microwave_shaders, oven_shaders)
+
+from infinigen.assets.materials import black_plastic
+from infinigen.assets.materials.art import ArtFabric, ArtRug, Art
+from infinigen.assets.materials.wear_tear import procedural_edge_wear, procedural_scratch
+from infinigen.assets.color_fits import real_color_distribution
+
+DEFAULT_EDGE_WEAR_PROB = .5
+DEFAULT_SCRATCH_PROB = .5
+
+class TextureAssignments:
+
+ def __init__(self, materials, probabilities):
+ self.materials = materials
+ self.probabilities = probabilities
+
+ def assign_material(self):
+ p = np.array(self.probabilities)
+ p = p / p.sum()
+ return np.random.choice(self.materials, p=p)
+
+class MaterialOptions:
+ def __init__(self, materials_list):
+ self.materials, self.probabilities = zip(*materials_list)
+ self.probabilities = np.array(self.probabilities)
+ self.probabilities = self.probabilities / self.probabilities.sum()
+
+ def assign_material(self):
+ return np.random.choice(self.materials, p=self.probabilities)
+
+
+def get_all_metal_shaders():
+ metal_shaders_list = [metal.brushed_metal.shader_brushed_metal,
+ metal.galvanized_metal.shader_galvanized_metal,
+ metal.grained_and_polished_metal.shader_grained_metal,
+ metal.hammered_metal.shader_hammered_metal]
+ color = metal.sample_metal_color()
+ new_shaders = [functools.partial(shader, base_color=color) for shader in metal_shaders_list]
+ for idx, ns in enumerate(new_shaders):
+ # fix taken from: https://github.com/elastic/apm-agent-python/issues/293
+ ns.__name__ = metal_shaders_list[idx].__name__
+
+ return new_shaders
+
+def plastic_furniture():
+ new_shader = functools.partial(shader_rough_plastic, base_color=real_color_distribution('sofa_leather'))
+ new_shader.__name__ = shader_rough_plastic.__name__
+ return new_shader
+
+
+def get_all_fabric_shaders():
+ return [fabrics.shader_coarse_fabric_random, fabrics.shader_fine_fabric_random, fabrics.shader_fabric,
+ fabrics.shader_leather, fabrics.shader_sofa_fabric]
+
+
+def beverage_fridge_materials():
+ metal_shaders = get_all_metal_shaders()
+ return {
+ "surface": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "front": TextureAssignments([beverage_fridge_shaders.shader_glass_001], [1.0]),
+ "handle": TextureAssignments([beverage_fridge_shaders.shader_white_metal_001], [1.0]),
+ "back": TextureAssignments([beverage_fridge_shaders.shader_black_medal_001], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def dishwasher_materials():
+ metal_shaders = get_all_metal_shaders()
+ return {
+ "surface": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "front": TextureAssignments([dishwasher_shaders.shader_glass_002], [1.0]),
+ "white_metal": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "top": TextureAssignments([dishwasher_shaders.shader_black_medal_002], [1.0]),
+ "name_material": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def microwave_materials():
+ metal_shaders = get_all_metal_shaders()
+ return {
+ "surface": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "back": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "black_glass": TextureAssignments([microwave_shaders.shader_black_glass], [1.0]),
+ "glass": TextureAssignments([microwave_shaders.shader_glass], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def oven_materials():
+ metal_shaders = get_all_metal_shaders()
+ return {
+ "surface": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "back": TextureAssignments([oven_shaders.shader_black_medal], [1.0]),
+ "white_metal": TextureAssignments(metal_shaders, [1.0]* len(metal_shaders)),
+ "black_glass": TextureAssignments([oven_shaders.shader_super_black_glass], [1.0]),
+ "glass": TextureAssignments([oven_shaders.shader_glass], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def tv_materials():
+ return {
+ "surface": TextureAssignments([metal, plastic_rough], [1.0, .2]),
+ "screen_surface": TextureAssignments([text.Text], [1.0]),
+ "support": TextureAssignments([metal, plastic_rough], [1.0, .2]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def bathtub_materials():
+ return {
+ "surface": TextureAssignments([ceramic], [1]),
+ "leg": TextureAssignments([metal], [1.0]),
+ "hole": TextureAssignments([metal], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def bathroom_sink_materials():
+ return {
+ "surface": TextureAssignments([ceramic, metal], [0.9, 0.1]),
+ # rest inherited from bathtub_materials
+ }
+
+def toilet_materials():
+ return {
+ "surface": TextureAssignments([ceramic, metal], [0.9, 0.1]),
+ "hardware_surface": TextureAssignments([metal], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def hardware_materials():
+ return {
+ "surface": TextureAssignments([metal], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def blanket_materials():
+ return {
+ "surface": TextureAssignments([ArtFabric, fabrics],
+ [1.0, 1.0]),
+ }
+
+def pants_materials():
+ return {
+ "surface": TextureAssignments([ArtFabric, fabrics],
+ [1.0, 1.0]),
+ }
+
+def towel_materials():
+ return {
+ "surface": TextureAssignments([ArtRug, rug],
+ [0.2, 0.8]),
+ }
+
+def acquarium_materials():
+ return {
+ "glass_surface": TextureAssignments([glass], [1.0]),
+ "belt_surface": TextureAssignments([metal.galvanized_metal], [1.0]),
+ "water_surface": TextureAssignments([water], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [0, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def ceiling_light_materials():
+ return {
+ "black_material": TextureAssignments([ceiling_light_shaders.shader_black], [1.0]),
+ "white_material": TextureAssignments([ceiling_light_shaders.shader_lamp_bulb_nonemissive], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB] }
+
+def lamp_materials():
+ return {
+ 'black_material': TextureAssignments([lamp_shaders.shader_black], [1.0]),
+ 'metal': TextureAssignments([lamp_shaders.shader_metal], [1.0]),
+ 'lampshade': TextureAssignments([lamp_shaders.shader_lampshade], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [0, 0]
+ }
+
+def table_cocktail_materials():
+ # top materials are: choice(['marble', 'tiled_wood', 'plastic', 'glass']),
+ # choice(['brushed_metal', 'grained_metal', 'galvanized_metal', 'wood', 'glass']),
+ metal_shaders = get_all_metal_shaders()
+ return {
+ 'top': TextureAssignments([table_marble.shader_marble,
+ woods.tiled_wood.shader_wood_tiled,
+ shader_rough_plastic,
+ glass_volume.shader_glass_volume],
+ [1.0, 1.0, 1.0, 1.0]),
+ 'leg': TextureAssignments([*metal_shaders,
+ wood.shader_wood,
+ glass_volume.shader_glass_volume],
+ [1.0] * len(metal_shaders)+ [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def table_dining_materials():
+ metal_shaders = get_all_metal_shaders()
+ fabric_shaders = get_all_fabric_shaders()
+ probs = [1.0 / len(metal_shaders)] * len(metal_shaders)
+
+ return {
+ 'top': MaterialOptions([
+ (table_marble.shader_marble, 2.0),
+ (wood.shader_wood, 1.0),
+ (dishwasher_shaders.shader_glass_002, 1.0),
+ (oven_shaders.shader_super_black_glass, 1.0),
+ (woods.tiled_wood.shader_wood_tiled, 2.0),
+ (glass_volume.shader_glass_volume, 1.0),
+ *(zip(metal_shaders, probs)),
+ ]),
+ 'leg': MaterialOptions([
+ (wood.shader_wood, 1.0),
+ (glass_volume.shader_glass_volume, 1.0),
+ (plastic_furniture(), 1.0),
+ *(zip(metal_shaders, probs)),
+ ]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def bar_chair_materials(leg_style=None):
+ metal_shaders = get_all_metal_shaders()
+ if leg_style == "wheeled":
+ probs = [0.01 / len(metal_shaders)] * len(metal_shaders)
+ else:
+ probs = [1.0 / len(metal_shaders)] * len(metal_shaders)
+ return {
+ 'seat': TextureAssignments([leather.shader_leather],
+ [1.0]),
+ 'leg': TextureAssignments([wood.shader_wood,
+ *metal_shaders],
+ [1.0] + probs),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0]
+ }
+
+def chair_materials():
+ return {
+ 'limb': TextureAssignments(
+ [metal, wood, fabrics],
+ [2.0, 2., 2]
+ ),
+ 'surface': TextureAssignments([plastic_rough, wood, fabrics], [.3, 0.5, 0.7]),
+ 'panel': TextureAssignments([plastic_rough, wood, fabrics], [.3, 0.5, 0.7]),
+ 'arm': TextureAssignments([plastic, wood, fabrics], [.3, 0.5, 0.7]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def office_chair_materials(leg_style=None):
+ metal_shaders = get_all_metal_shaders()
+ if leg_style == "wheeled":
+ probs = [0.01 / len(metal_shaders)] * len(metal_shaders)
+ else:
+ probs = [1.0 / len(metal_shaders)] * len(metal_shaders)
+ return {
+ 'top': TextureAssignments([
+ leather.shader_leather,
+ wood.shader_wood,
+ shader_rough_plastic,
+ glass_volume.shader_glass_volume],
+ [1.0, 1.0, 1.0, 1.0]),
+ 'leg': TextureAssignments([wood.shader_wood,
+ *metal_shaders],
+ [1.0] + probs),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def bedframe_materials():
+ return {
+ 'surface': TextureAssignments([wood, plaster],
+ [2.0, 1.0,]),
+ 'limb_surface': TextureAssignments([wood, plaster], [2.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def mattress_materials():
+ return {
+ 'surface': TextureAssignments([sofa_fabric],
+ [1.0]),
+ }
+
+def pillow_materials():
+ return {
+ 'surface': TextureAssignments([ArtFabric, sofa_fabric],
+ [1.0, 1.0]),
+ }
+
+def sofa_materials():
+ return {
+ 'sofa_fabric': MaterialOptions([
+ (velvet.shader_velvet, 0.5),
+ (sofa_fabric.shader_sofa_fabric, 0.3),
+ (leather.shader_leather, 0.2)
+ ]),
+ }
+
+def book_materials():
+ return {
+ "surface": TextureAssignments([plaster], [1.0]),
+ "cover_surface": TextureAssignments([text.Text], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [0, 0]
+ }
+
+def vase_materials():
+ return {
+ "surface": TextureAssignments([vase_shaders.shader_ceramic,
+ glass_volume.shader_glass_volume],
+ [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def pan_materials():
+ return {
+ "surface": TextureAssignments([metal], [1.0]),
+ "inside": TextureAssignments([metal], [1.0]),
+ # no guard as it overrides over tableware_materials
+ }
+
+def cup_materials():
+ return {
+ 'surface': TextureAssignments([glass, plastic], [1.0, 1.0]),
+ "wrap_surface": TextureAssignments([text.Text], [1.0]),
+ }
+
+def bottle_materials():
+ return {
+ "surface": TextureAssignments([glass, plastic], [1.0, 1.0]),
+ "wrap_surface": TextureAssignments([text.Text], [1.0]),
+ "cap_surface": TextureAssignments([metal, plastic], [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0]
+ }
+
+def tableware_materials(fragile=False, transparent=False):
+ if fragile:
+ surface_materials = TextureAssignments([ceramic, glass, plastic], [1.0, 1, 1])
+ elif transparent:
+ surface_materials = TextureAssignments([ceramic, glass], [1.0, 1])
+ else:
+ surface_materials = TextureAssignments([ceramic, glass, plastic, metal, wood], [1, 1, 1.0, 1, 1])
+
+ return {
+ "surface": surface_materials,
+ "guard": TextureAssignments([wood, plastic], [1.0, 1.0]),
+ "inside": TextureAssignments([ceramic, metal], [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+
+def can_materials():
+ return {
+ "surface": TextureAssignments([metal], [1.0]),
+ "wrap_surface": TextureAssignments([text.Text], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def jar_materials():
+ return {
+ "surface": TextureAssignments([glass], [1.0]),
+ "cap_surface": TextureAssignments([metal], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def foodbag_materials():
+ return {
+ "surface": TextureAssignments([text.Text], [1.0]),
+ }
+
+def lid_materials():
+ return {
+ "surface": TextureAssignments([ceramic, metal], [0.5, 0.5]),
+ "rim_surface": TextureAssignments([metal], [1.0]),
+ "handle_surface": TextureAssignments([metal, ceramic], [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0]
+ }
+def glasslid_materials():
+ return {
+ "surface": TextureAssignments([glass], [1.0]),
+ "rim_surface": TextureAssignments([metal], [1.0]),
+ "handle_surface": TextureAssignments([metal, ceramic], [1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0]
+ }
+
+def plant_container_materials():
+ return {
+ "surface": TextureAssignments([ceramic, metal], [3., 1.]),
+ 'dirt_surface': TextureAssignments([dirt], [1.0]),
+ }
+
+def balloon_materials():
+ return {
+ "surface": TextureAssignments([metal],
+ [1.0]),
+ }
+
+def range_hood_materials():
+ return {
+ "surface": TextureAssignments([metal], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def wall_art_materials():
+ return {
+ "frame": TextureAssignments([wood, metal], [1.0, 1.0]),
+ "surface": TextureAssignments([Art], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def mirror_materials():
+ return {
+ "frame": TextureAssignments([wood, metal], [1.0, 1.0]),
+ "surface": TextureAssignments([mirror], [1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+
+def kitchen_sink_materials():
+ shaders = get_all_metal_shaders()
+ sink_color = metal.sample_metal_color(metal_color='natural')
+ if uniform() < .5:
+ tap_color = metal.sample_metal_color(metal_color='plain')
+ else:
+ tap_color = metal.sample_metal_color(metal_color='natural')
+ sink_shaders = [lambda nw, *args: shader(nw, *args, base_color=sink_color) for shader in shaders]
+ tap_shaders = [lambda nw, *args: shader(nw, *args, base_color=tap_color) for shader in shaders]
+ return {
+ "sink": TextureAssignments(sink_shaders, [1.0, 1.0, 1.0, 1.0]),
+ "tap": TextureAssignments(tap_shaders, [1.0, 1.0, 1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+
+def kitchen_tap_materials():
+ shaders = get_all_metal_shaders()
+ if uniform() < .5:
+ tap_color = metal.sample_metal_color(metal_color='plain')
+ else:
+ tap_color = metal.sample_metal_color(metal_color='natural')
+ tap_shaders = [lambda nw, *args: shader(nw, *args, base_color=tap_color) for shader in shaders]
+ return {
+ "tap": TextureAssignments(tap_shaders, [1.0, 1.0, 1.0, 1.0]),
+ "wear_tear": [procedural_scratch, procedural_edge_wear],
+ "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB]
+ }
+
+def rug_materials():
+ return {
+ "surface": MaterialOptions([
+ (rug, 3.0),
+ (ArtRug, 2.0),
+ (fabrics, 5.0),
+ ])
+ }
+
+AssetList = {
+ # appliances
+ 'BeverageFridgeFactory': beverage_fridge_materials, # looks like dishwasher currently
+ 'DishwasherFactory': dishwasher_materials,
+ 'MicrowaveFactory': microwave_materials,
+ 'OvenFactory': oven_materials, # looks like dishwasher currently
+ 'TVFactory': tv_materials,
+ 'MonitorFactory': None, # inherits from TVFactory
+ # bathroom
+ 'BathtubFactory': bathtub_materials,
+ 'BathroomSinkFactory': bathroom_sink_materials, # inheriting from bathtub factory, so not used
+ 'HardwareFactory': hardware_materials,
+ 'ToiletFactory': toilet_materials,
+ # clothes
+ 'BlanketFactory': blanket_materials, # has Art which is a class, not func,
+ # also "Normal Not Found" is printed when generating
+ ############## this point onwards, using this dictionary to get corresponding
+ ############## material functions except for tableware base
+ 'PantsFactory': pants_materials, # same comment as above
+ 'ShirtFactory': pants_materials, # same comment as above
+ 'TowelFactory': towel_materials,
+ # decor
+ 'AquariumTankFactory': acquarium_materials,
+ # lighting
+ 'CausticsLampFactory': None, # the properties are not materials, so skipping
+ 'CeilingLightFactory': ceiling_light_materials,
+ 'PointLampFactory': None, # the properties are not materials, so skipping
+ 'LampFactory': lamp_materials, # really required bunch of changes to expose the materials
+ # seating: chairs
+ 'BarChairFactory': bar_chair_materials,
+ 'ChairFactory': chair_materials, # an internal reassignment that overrides surface with the limb material
+ 'OfficeChairFactory': office_chair_materials,
+ # seating: sofas and beds
+ 'BedFactory': None, # uses the below factories, so no materials
+ 'BedFrameFactory': bedframe_materials,
+ 'MattressFactory': mattress_materials,
+ 'PillowFactory': pillow_materials,
+ 'SofaFactory': sofa_materials,
+ # shelves: todo
+ 'SimpleDeskFactory': None,
+ 'SimpleBookcaseFactory': None,
+ 'CellShelfFactory': None,
+ 'TVStandFactory': None,
+ 'TriangleShelfFactory': None,
+ 'LargeShelfFactory': None,
+ 'SingleCabinetFactory': None,
+ 'KitchenCabinetFactory': None,
+ 'KitchenSpaceFactory': None,
+ 'KitchenIslandFactory': None,
+ # table decorations : they have their own materials
+ 'BookFactory': book_materials,
+ 'BookColumnFactory': None, # use BookFactory
+ 'BookStackFactory': None, # use BookFactory
+ 'VaseFactory': vase_materials,
+ # sink and tap
+ 'SinkFactory': kitchen_sink_materials,
+ 'TapFactory': kitchen_tap_materials,
+ # tables
+ 'TableCocktailFactory': table_cocktail_materials,
+ 'TableDiningFactory': table_dining_materials,
+ 'TableTopFactory': None, # not sure where the materials are used in it
+ # Tableware
+ 'TablewareFactory': tableware_materials, # only function with arguments
+ # 'TablewareFactory': tableware_materials_default, # directly uses the following functions (not through the AssetList Dictionary)
+ 'SpoonFactory': None, # uses materials from tableware base
+ 'KnifeFactory': None, # uses materials from tableware base
+ 'ChopsticksFactory': None, # uses materials from tableware base
+ 'ForkFactory': None, # uses materials from tableware base
+ 'SpatulaFactory': None, # uses materials from tableware base
+ 'PanFactory': pan_materials,
+ 'PotFactory': None, # uses the same materials as PanFactory
+ 'CupFactory': cup_materials,
+ 'WineglassFactory': None, # uses materials from transparent tableware
+ 'PlateFactory': None, # uses materials from tableware base
+ 'BowlFactory': None, # uses materials from tableware base
+ 'FruitContainerFactory': None, # uses materials from tableware base
+ 'BottleFactory': bottle_materials,
+ 'CanFactory': can_materials,
+ 'JarFactory': jar_materials,
+ 'FoodBagFactory': foodbag_materials,
+ 'FoodBoxFactory': foodbag_materials, # same params as above
+ 'LidFactory': lid_materials,
+ 'GlassLidFactory': glasslid_materials,
+ 'PlantContainerFactory': plant_container_materials,
+ # wall decorations
+ 'BalloonFactory': balloon_materials,
+ 'RangeHoodFactory': range_hood_materials, # getting RangeHoodFactory not Found.
+ 'WallArtFactory': wall_art_materials,
+ 'MirrorFactory': mirror_materials,
+ # window
+ 'WindowFactory': None,
+ "RugFactory": rug_materials,
+}
diff --git a/infinigen/assets/materials/__init__.py b/infinigen/assets/materials/__init__.py
index 89abe7d35..039b38d05 100644
--- a/infinigen/assets/materials/__init__.py
+++ b/infinigen/assets/materials/__init__.py
@@ -1,2 +1,55 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
from . import *
from infinigen.infinigen_gpl.surfaces import *
+
+from .metal.brushed_metal import shader_brushed_metal
+from .metal.galvanized_metal import shader_galvanized_metal
+from .metal.grained_and_polished_metal import shader_grained_metal
+from .metal.hammered_metal import shader_hammered_metal
+from .metal.metal_basic import shader_metal
+from .metal import (
+ metal_basic,
+ galvanized_metal,
+ grained_and_polished_metal,
+ hammered_metal,
+ brushed_metal,
+)
+
+metal_shader_list = [
+ shader_brushed_metal,
+ shader_galvanized_metal,
+ shader_grained_metal,
+ shader_hammered_metal,
+ shader_metal,
+]
+
+from .plastic import shader_rough_plastic
+from .plastic import shader_translucent_plastic
+
+plastic_shader_list = [shader_rough_plastic, shader_translucent_plastic]
+
+from .woods.wood import shader_wood
+
+wood_shader_list = [shader_wood]
+
+from .glass import shader_glass
+
+glass_shader_list = [shader_glass]
+
+from .leather_and_fabrics import (
+ leather,
+ general_fabric,
+ sofa_fabric,
+ fine_knit_fabric,
+ coarse_knit_fabric,
+ lined_fabric,
+ velvet
+)
+from .art import Art, DarkArt, ArtRug, ArtFabric
+from .stone_and_concrete import concrete
+from .woods import tiled_wood, wood, wood_old, square_wood_tile, hexagon_wood_tile, composite_wood_tile, \
+ staggered_wood_tile, crossed_wood_tile, wood_tile, non_wood_tile
diff --git a/infinigen/assets/materials/art.py b/infinigen/assets/materials/art.py
new file mode 100644
index 000000000..a872158bd
--- /dev/null
+++ b/infinigen/assets/materials/art.py
@@ -0,0 +1,100 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from . import text
+from ..utils.decorate import read_uv, write_uv
+from ...core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.random import random_general as rg
+
+
+class Art(text.Text):
+ def __init__(self, factory_seed):
+ super().__init__(factory_seed)
+ with FixedSeed(self.factory_seed):
+ self.n_barcodes = 0
+ self.n_texts = 0
+ self.n_patches = np.random.randint(10, 15)
+
+ @staticmethod
+ def scale_uniform(min_, max_):
+ return (max_ - min_) * log_uniform(.1, .5)
+
+
+class DarkArt(Art):
+
+ def __init__(self, factory_seed):
+ super().__init__(factory_seed)
+ with FixedSeed(self.factory_seed):
+ self.darken_scale = uniform(5, 10)
+ self.darken_ratio = uniform(.5, 1)
+
+ def make_shader_func(self, bbox):
+ art_shader_func = super(DarkArt, self).make_shader_func(bbox)
+
+ def shader_dark_art(nw: NodeWrangler):
+ art_shader_func(nw)
+ art_bsdf = nw.find(Nodes.PrincipledBSDF)[0]
+ art_color = nw.find_from(art_bsdf.inputs[0])[0].from_socket
+ dark_color = nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': self.darken_scale}).outputs[0]
+ art_color = nw.new_node(
+ Nodes.MixRGB, [self.darken_ratio, art_color, dark_color],
+ attrs={'blend_type': 'DARKEN'}
+ ).outputs[2]
+ nw.connect_input(art_color, art_bsdf.inputs[0])
+
+ return shader_dark_art
+
+
+class ArtComposite(DarkArt):
+
+ @property
+ def base_shader(self):
+ raise NotImplementedError
+
+ def make_shader_func(self, bbox):
+ art_shader_func = super(ArtComposite, self).make_shader_func(bbox)
+
+ def shader_art_composite(nw: NodeWrangler, **kwargs):
+ self.base_shader(nw, **kwargs)
+ nw_, base_bsdf = nw.find_recursive(Nodes.PrincipledBSDF)[-1]
+ art_shader_func(nw_)
+ art_bsdf = nw_.find(Nodes.PrincipledBSDF)[-1]
+ art_color = nw_.find_from(art_bsdf.inputs[0])[0].from_socket
+ nw_.nodes.remove(art_bsdf)
+ nw_.connect_input(art_color, base_bsdf.inputs[0])
+ nw_.connect_input(base_bsdf.outputs[0], nw_.find(Nodes.MaterialOutput)[0].inputs['Surface'])
+
+ return shader_art_composite
+
+ def make_sphere(self):
+ return make_sphere()
+
+
+class ArtRug(ArtComposite):
+ @property
+ def base_shader(self):
+ from . import rug
+ return rug.shader_rug
+
+
+class ArtFabric(ArtComposite):
+ @property
+ def base_shader(self):
+ from .leather_and_fabrics import fabric_shader_list
+ return rg(fabric_shader_list)
+
+
+def apply(obj, selection=None, bbox=(0, 1, 0, 1), scale=None, **kwargs):
+ if scale is not None:
+ write_uv(obj, read_uv(obj) * scale)
+ Art(np.random.randint(1e5)).apply(obj, selection, bbox, **kwargs)
+
+
+def make_sphere():
+ return text.make_sphere()
diff --git a/infinigen/assets/materials/beverage_fridge_shaders.py b/infinigen/assets/materials/beverage_fridge_shaders.py
new file mode 100644
index 000000000..2499733e9
--- /dev/null
+++ b/infinigen/assets/materials/beverage_fridge_shaders.py
@@ -0,0 +1,46 @@
+ # Copyright (c) Princeton University.
+ # This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+ # Authors: Hongyu Wen
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+def shader_glass_001(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ glass_bsdf = nw.new_node(Nodes.GlassBSDF, input_kwargs={'IOR': 1.5000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True})
+
+
+
+def shader_black_medal_001(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ anisotropic_bsdf = nw.new_node('ShaderNodeBsdfAnisotropic', input_kwargs={'Color': (0.0167, 0.0167, 0.0167, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': anisotropic_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_white_metal_001(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (1.0000, 1.0000, 50.0000)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 20.0000, 'Detail': 20.0000, 'Distortion': 1.0000})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Color"]})
+ colorramp.color_ramp.elements[0].position = 0.2500
+ colorramp.color_ramp.elements[0].color = [0.5244, 0.5244, 0.5244, 1.0000]
+ colorramp.color_ramp.elements[1].position = 1.0000
+ colorramp.color_ramp.elements[1].color = [0.9698, 0.9698, 0.9698, 1.0000]
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"], 'Subsurface Color': (1.0000, 1.0000, 1.0000, 1.0000), 'Metallic': 1.0000, 'Specular': 1.0000, 'Roughness': 0.1000, 'Anisotropic': 0.9182, 'Sheen': 0.0455, 'Sheen Tint': 0.4948},
+ attrs={'subsurface_method': 'BURLEY'})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/black_plastic.py b/infinigen/assets/materials/black_plastic.py
new file mode 100644
index 000000000..8a8a3a446
--- /dev/null
+++ b/infinigen/assets/materials/black_plastic.py
@@ -0,0 +1,22 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from numpy.random import uniform as U
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.util.color import hsv2rgba
+
+# used in ceiling lights and tv
+
+def shader_black(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ color = hsv2rgba(
+ U(0.45, 0.55),
+ U(0, 0.1),
+ U(0, 1)
+ )
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': color})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/brick.py b/infinigen/assets/materials/brick.py
new file mode 100644
index 000000000..20e45873c
--- /dev/null
+++ b/infinigen/assets/materials/brick.py
@@ -0,0 +1,64 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections.abc import Iterable
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.object import new_plane
+from infinigen.assets.utils.uv import unwrap_faces, unwrap_normal
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+from infinigen.assets.materials import common
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+
+def shader_brick(nw: NodeWrangler, height=None, **kwargs):
+ if height is None:
+ height = log_uniform(.07, .12)
+ uv_map = nw.new_node(Nodes.UVMap)
+
+ front_color, back_color = [hsv2rgba(uniform(0, .05), uniform(.8, 1), log_uniform(.02, .5)) for _ in
+ range(2)]
+ mortar_color = hsv2rgba(uniform(0, .05), uniform(.2, .5), log_uniform(.02, .8))
+ dark_color = hsv2rgba(uniform(0, .05), uniform(.8, 1), log_uniform(.005, .02))
+ noise = nw.new_node(Nodes.NoiseTexture, [uv_map],
+ input_kwargs={'Scale': uniform(40, 50), 'Detail': uniform(15, 20)})
+ color = nw.new_node(Nodes.BrickTexture, [uv_map, front_color, back_color, mortar_color], input_kwargs={
+ 'Scale': 1,
+ 'Row Height': height,
+ 'Brick Width': height * log_uniform(1.2, 2.5),
+ 'Mortar Size': height * log_uniform(.04, .08),
+ 'Mortar Smooth': noise
+ }).outputs['Color']
+ noise = nw.new_node(Nodes.MusgraveTexture, [uv_map], input_kwargs={'Scale': uniform(2, 5)})
+ color = nw.new_node(Nodes.MixRGB, [nw.scalar_multiply(log_uniform(.5, 1.), noise), color, dark_color],
+ attrs={'blend_type': 'DARKEN'})
+
+ roughness = nw.build_float_curve(nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': 50}),
+ [(0, .5), (1, 1.)])
+
+ offset = nw.scalar_add(nw.scalar_multiply(nw.scalar_sub(color, .5), uniform(.01, .04)), nw.scalar_multiply(
+ nw.new_node(Nodes.MusgraveTexture, [uv_map], input_kwargs={'Scale': 50}), uniform(.0, .01)))
+ bump = nw.new_node(Nodes.Bump, input_kwargs={'Height': offset})
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={"Roughness": roughness, 'Base Color': color, 'Normal': bump
+ })
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf})
+
+
+def apply(obj, selection=None, height=None, **kwargs):
+ for o in obj if isinstance(obj, Iterable) else [obj]:
+ unwrap_normal(o, selection, axis_='z')
+ common.apply(obj, shader_brick, selection, height, **kwargs)
+
+
+def make_sphere():
+ obj = new_plane()
+ obj.rotation_euler[0] = np.pi / 2
+ butil.apply_transform(obj, True)
+ return obj
diff --git a/infinigen/assets/materials/bumpy_rubber_floor.py b/infinigen/assets/materials/bumpy_rubber_floor.py
new file mode 100644
index 000000000..ee5fa8cdb
--- /dev/null
+++ b/infinigen/assets/materials/bumpy_rubber_floor.py
@@ -0,0 +1,176 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+
+@node_utils.to_nodegroup('nodegroup_node_group', singleton=False, type='ShaderNodeTree')
+def nodegroup_node_group(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 1.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloatFactor', 'Roughness', 0.4000)])
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Generated"], 'Scale': group_input.outputs["Scale"]})
+
+ reroute_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': mapping})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Seed"]})
+
+ noise_texture_9 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': reroute_1, 'W': reroute, 'Scale': 18.0000, 'Detail': 3.0000, 'Roughness': 0.4500},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_6 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_9.outputs["Fac"], 3: 0.6000, 4: 1.4000})
+
+ hue_saturation_value = nw.new_node(Nodes.HueSaturationValue,
+ input_kwargs={'Value': map_range_6.outputs["Result"], 'Color': group_input.outputs["Base Color"]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': hue_saturation_value, 'Specular': 0.9, 'Roughness': group_input.outputs["Roughness"]})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 2.0000, 'Detail': 6.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'Randomness': 0.0000},
+ attrs={'feature': 'DISTANCE_TO_EDGE', 'voronoi_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': voronoi_texture.outputs["Distance"], 2: 0.0300, 3: 1.0000, 4: 0.0000})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 2.5000, 'Detail': 6.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_1.outputs["Fac"], 1: 0.5500, 2: 0.5700})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: map_range.outputs["Result"], 1: map_range_1.outputs["Result"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_2 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 10.0000, 'Detail': 15.0000, 'Distortion': 0.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_2.outputs["Fac"], 1: 0.6300, 2: 0.6800})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: map_range_2.outputs["Result"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: multiply_2})
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 200.0000
+
+ noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': value},
+ attrs={'noise_dimensions': '4D'})
+
+ voronoi_texture_1 = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': value},
+ attrs={'voronoi_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.4000, 2: noise_texture_3.outputs["Fac"], 3: voronoi_texture_1.outputs["Distance"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: mix.outputs["Result"], 1: 0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: multiply_3})
+
+ noise_texture_4 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 4.0000, 'Detail': 1.0000, 'Roughness': 0.4500},
+ attrs={'noise_dimensions': '4D'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: noise_texture_4.outputs["Fac"]}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: 3.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_4})
+
+ noise_texture_5 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 40.0000, 'Detail': 15.0000, 'Distortion': 0.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_3 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': noise_texture_5.outputs["Fac"], 1: 0.6500, 2: 0.6400, 3: 1.0000, 4: 0.0000})
+
+ noise_texture_7 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group_input.outputs["Seed"], 'Scale': 12.0000, 'Detail': 6.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_4 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_7.outputs["Fac"], 1: 0.5500, 2: 0.5700})
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: map_range_3.outputs["Result"], 1: map_range_4.outputs["Result"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_5}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: multiply_6})
+
+ noise_texture_6 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': reroute_1, 'W': reroute, 'Scale': 30.0000, 'Detail': 3.0000, 'Roughness': 0.4500},
+ attrs={'noise_dimensions': '4D'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: noise_texture_6.outputs["Fac"]}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2}, attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: multiply_7})
+
+ noise_texture_8 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': reroute_1, 'W': reroute, 'Scale': 20.0000, 'Detail': 3.0000, 'Roughness': 0.4500},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range_5 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': noise_texture_8.outputs["Fac"], 1: 0.5500, 2: 0.5100, 3: -0.5000, 4: 0.5000})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={0: map_range_5.outputs["Result"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_8})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': add_5, 'tmp_viewer': principled_bsdf},
+ attrs={'is_active_output': True})
+
+def shader_bumpy_rubber(nw: NodeWrangler, scale=2.0, base_color=None, seed=None):
+ # Code generated using version 2.6.5 of the node_transpiler
+ if base_color is None:
+ base_color = hsv2rgba(uniform(0, 1), uniform(.2, .5), uniform(.4, .7))
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+
+ roughness = uniform(0.1, 0.3)
+
+ group = nw.new_node(nodegroup_node_group().name,
+ input_kwargs={'Base Color': base_color, 'Scale': scale, 'Seed': seed, 'Roughness': roughness})
+
+ displacement = nw.new_node(Nodes.Displacement,
+ input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000, 'Scale': 0.0010})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["tmp_viewer"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_bumpy_rubber, selection=selection)
\ No newline at end of file
diff --git a/infinigen/assets/materials/ceiling_light_shaders.py b/infinigen/assets/materials/ceiling_light_shaders.py
new file mode 100644
index 000000000..d738d8230
--- /dev/null
+++ b/infinigen/assets/materials/ceiling_light_shaders.py
@@ -0,0 +1,45 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from numpy.random import uniform as U
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.util.color import hsv2rgba
+
+
+def shader_lamp_bulb_nonemissive(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ light_path = nw.new_node(Nodes.LightPath)
+
+ object_info = nw.new_node(Nodes.ObjectInfo_Shader)
+
+ white_noise_texture = nw.new_node(Nodes.WhiteNoiseTexture,
+ input_kwargs={'Vector': object_info.outputs["Random"]},
+ attrs={'noise_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: white_noise_texture.outputs["Color"], 7: (0.5000, 0.4444, 0.3669, 1.0000)},
+ attrs={'data_type': 'RGBA'})
+
+ transparent_bsdf = nw.new_node(Nodes.TransparentBSDF, input_kwargs={'Color': mix.outputs[2]})
+
+ translucent_bsdf = nw.new_node(Nodes.TranslucentBSDF, input_kwargs={'Color': mix.outputs[2]})
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': light_path.outputs["Is Camera Ray"], 1: transparent_bsdf, 2: translucent_bsdf})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': mix_shader}, attrs={'is_active_output': True})
+
+def shader_black(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ color = hsv2rgba(
+ U(0.45, 0.55),
+ U(0, 0.1),
+ U(0, 1)
+ )
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': color})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/materials/ceramic.py b/infinigen/assets/materials/ceramic.py
new file mode 100644
index 000000000..40e63c96f
--- /dev/null
+++ b/infinigen/assets/materials/ceramic.py
@@ -0,0 +1,44 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from numpy.random import uniform
+
+from infinigen.core.util.color import hsv2rgba
+from infinigen.assets.materials import common
+from infinigen.core.util.random import log_uniform
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+
+
+def shader_ceramic(nw: NodeWrangler, clear=False, roughness_min=0, roughness_max=.8, **kwargs):
+ if uniform(0, 1) < .8 and not clear:
+ color = hsv2rgba(uniform(0, 1), uniform(.2, .4), log_uniform(.3, .6))
+ else:
+ color = hsv2rgba(0, 0, log_uniform(.3, .6))
+
+ roughness = nw.build_float_curve(nw.musgrave(log_uniform(20, 40)), [(0, roughness_min), (1, roughness_max)])
+ clearcoat_roughness = nw.build_float_curve(nw.musgrave(log_uniform(20, 40)),
+ [(0, roughness_min), (1, roughness_max)])
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ "Roughness": roughness,
+ 'Clearcoat': 1,
+ 'Clearcoat Roughness': clearcoat_roughness,
+ 'Specular': 1,
+ 'Base Color': color,
+ 'Subsurface': uniform(.02, .05),
+ 'Subsurface Radius': (.02, .02, .02)
+ })
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={
+ 'Height': nw.scalar_multiply(log_uniform(.001, .005), nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Scale': log_uniform(20, 40)})),
+ 'Midlevel': 0.0000
+ })
+
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement})
+
+
+def apply(obj, selection=None, clear=False, **kwargs):
+ common.apply(obj, shader_ceramic, selection, clear, **kwargs)
diff --git a/infinigen/assets/materials/chunkyrock.py b/infinigen/assets/materials/chunkyrock.py
index 9fedacc32..6d0687273 100644
--- a/infinigen/assets/materials/chunkyrock.py
+++ b/infinigen/assets/materials/chunkyrock.py
@@ -25,24 +25,24 @@
mod_name = "geo_rocks"
name = "chunkyrock"
-def shader_rocks(nw, rand=True, **input_kwargs):
+def shader_rocks(nw, rand=True, random_seed=0, **input_kwargs):
nw.force_input_consistency()
position = nw.new_node('ShaderNodeNewGeometry')
- depth = geo_rocks(nw, geometry=False)
-
+ depth = geo_rocks(nw, random_seed=random_seed, geometry=False)
+
colorramp_3 = nw.new_node(Nodes.ColorRamp,
input_kwargs={'Fac': depth})
colorramp_3.color_ramp.elements[0].position = 0.0285
colorramp_3.color_ramp.elements[0].color = (0.0, 0.0, 0.0, 1.0)
colorramp_3.color_ramp.elements[1].position = 0.1347
colorramp_3.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
-
+
mapping = nw.new_node(Nodes.Mapping,
input_kwargs={'Vector': position, 'Scale': (0.2, 0.2, 0.2)})
-
+
noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': mapping, 'Detail': 15.0})
-
+
rock_color1 = nw.new_node(Nodes.MixRGB,
input_kwargs={'Fac': noise_texture_1.outputs["Fac"], 'Color1': (0.0, 0.0, 0.0, 1.0), 'Color2': (0.01, 0.024, 0.0283, 1.0)})
@@ -62,7 +62,7 @@ def shader_rocks(nw, rand=True, **input_kwargs):
mix_1 = nw.new_node(Nodes.MixRGB,
input_kwargs={'Fac': colorramp_3.outputs["Color"], 'Color1': rock_color1, 'Color2': rock_color2})
-
+
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
input_kwargs={'Base Color': mix_1})
@@ -77,16 +77,16 @@ def geo_rocks(nw: NodeWrangler, rand=True, selection=None, random_seed=0, geomet
else:
position = nw.new_node(Nodes.InputPosition)
normal = nw.new_node(Nodes.InputNormal)
-
+
with FixedSeed(random_seed):
# Code generated using version 2.4.3 of the node_transpiler
-
+
noise_texture = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': position})
-
+
mix = nw.new_node(Nodes.MixRGB,
input_kwargs={'Fac': 0.8, 'Color1': noise_texture.outputs["Color"], 'Color2': position})
-
+
if rand:
sample_max = 2
sample_min = 1/2
@@ -112,19 +112,19 @@ def geo_rocks(nw: NodeWrangler, rand=True, selection=None, random_seed=0, geomet
colorramp.color_ramp.elements[1].position = sample_ratio(colorramp.color_ramp.elements[1].position, 0.5, 2)
depth = colorramp
-
+
multiply = nw.new_node(Nodes.VectorMath,
input_kwargs={0: colorramp.outputs["Color"], 1: normal},
attrs={'operation': 'MULTIPLY'})
-
+
value = nw.new_node(Nodes.Value)
value.outputs[0].default_value = 0.4
-
+
offset = nw.new_node(Nodes.VectorMath,
input_kwargs={0: multiply.outputs["Vector"], 1: value},
attrs={'operation': 'MULTIPLY'})
-
-
+
+
if geometry:
groupinput = nw.new_node(Nodes.GroupInput)
noise_params = {"scale": ("uniform", 10, 20), "detail": 9, "roughness": 0.6, "zscale": ("log_uniform", 0.08, 0.12)}
@@ -152,4 +152,4 @@ def apply(obj, selection=None, geo_kwargs=None, shader_kwargs=None, **kwargs):
#bpy.ops.wm.save_as_mainfile(filepath=fn)
bpy.context.scene.render.filepath = os.path.join('outputs', mat, '%s_%d.jpg'%(mat, i))
bpy.context.scene.render.image_settings.file_format='JPEG'
- bpy.ops.render.render(write_still=True)
\ No newline at end of file
+ bpy.ops.render.render(write_still=True)
diff --git a/infinigen/assets/materials/cobble_stone.py b/infinigen/assets/materials/cobble_stone.py
index c50e9d30c..a88f37b25 100644
--- a/infinigen/assets/materials/cobble_stone.py
+++ b/infinigen/assets/materials/cobble_stone.py
@@ -18,10 +18,10 @@
name = "cobble_stone"
-def shader_cobblestone(nw: NodeWrangler):
+def shader_cobblestone(nw: NodeWrangler, random_seed=0):
# Code generated using version 2.4.3 of the node_transpiler, and modified
nw.force_input_consistency()
- stone_color = geo_cobblestone(nw, geometry=False)
+ stone_color = geo_cobblestone(nw, random_seed=random_seed, geometry=False)
noise_texture = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': nw.new_node('ShaderNodeNewGeometry'), 'Scale': N(10, 1.5) / 25, 'W': U(-5, 5)},
attrs={'noise_dimensions': '4D'})
@@ -154,4 +154,4 @@ def geo_cobblestone(nw: NodeWrangler, selection=None, random_seed=0, geometry=Tr
def apply(obj, selection=None, **kwargs):
surface.add_geomod(obj,geo_cobblestone, selection=selection)
- surface.add_material(obj, shader_cobblestone, selection=selection, reuse=False)
\ No newline at end of file
+ surface.add_material(obj, shader_cobblestone, selection=selection, reuse=False)
diff --git a/infinigen/assets/materials/common.py b/infinigen/assets/materials/common.py
new file mode 100644
index 000000000..b906c4bcf
--- /dev/null
+++ b/infinigen/assets/materials/common.py
@@ -0,0 +1,68 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import functools
+from collections.abc import Callable, Iterable
+
+import numpy as np
+
+from infinigen.assets.utils.decorate import read_material_index, write_material_index
+from infinigen.core import surface
+from infinigen.core.surface import read_attr_data
+
+from infinigen.core import tags as t, tagging
+from infinigen.core.util.math import FixedSeed
+
+
+def apply(obj, shader_func, selection=None, *args, **kwargs):
+ if not isinstance(obj, Iterable):
+ obj = [obj]
+ if isinstance(shader_func, Callable):
+ material = surface.shaderfunc_to_material(shader_func, *args, **kwargs)
+ else:
+ material = shader_func
+ for o in obj:
+ index = len(o.data.materials)
+ o.data.materials.append(material)
+ material_index = read_material_index(o)
+ full_like = np.full_like(material_index, index)
+ if selection is None:
+ material_index = full_like
+ elif isinstance(selection, t.Tag):
+ sel = tagging.tagged_face_mask(o, selection)
+ material_index = np.where(sel, index, material_index)
+ elif isinstance(selection, str):
+ try:
+ sel = read_attr_data(o, selection.lstrip('!'), 'FACE')
+ material_index = np.where(1 - sel if selection.startswith('!') else sel, index, material_index)
+ except:
+ material_index = np.zeros(len(material_index), dtype=int)
+ else:
+ material_index = np.where(selection, index, material_index)
+ write_material_index(o, material_index)
+
+
+def get_selection(obj, selection):
+ if selection is None:
+ return np.ones(len(obj.data.polygons))
+ elif isinstance(selection, t.Tag):
+ return tagging.tagged_face_mask(obj, selection)
+ elif isinstance(selection, str):
+ return read_attr_data(obj, selection.lstrip('!'), 'FACE')
+ else:
+ return selection
+
+
+def unique_surface(surface, seed=None):
+ if seed is None:
+ seed = np.random.randint(1e7)
+
+ class Surface:
+
+ @classmethod
+ def apply(cls, *args, **kwargs):
+ with FixedSeed(seed):
+ return surface.apply(*args, **kwargs)
+
+ return Surface
diff --git a/infinigen/assets/materials/dirt.py b/infinigen/assets/materials/dirt.py
index 27f872ded..452731ca7 100644
--- a/infinigen/assets/materials/dirt.py
+++ b/infinigen/assets/materials/dirt.py
@@ -22,9 +22,9 @@
name = "dirt"
-def shader_dirt(nw):
+def shader_dirt(nw, random_seed=0):
nw.force_input_consistency()
- dirt_base_color, dirt_roughness = geo_dirt(nw, selection=None, geometry=False)
+ dirt_base_color, dirt_roughness = geo_dirt(nw, selection=None, random_seed=random_seed, geometry=False)
principled_bsdf = nw.new_node(
Nodes.PrincipledBSDF,
input_kwargs={
@@ -108,7 +108,7 @@ def geo_dirt(nw, selection=None, random_seed=0, geometry=True):
# label = "color_ramp_1_VAR",
attrs={'clamp': True}
)
-
+
#nw.new_node(
# Nodes.ColorRamp, input_kwargs={"Fac": voronoi_texture.outputs["Distance"]},
# label="color_ramp_1_VAR"
@@ -168,30 +168,30 @@ def geo_dirt(nw, selection=None, random_seed=0, geometry=True):
input_kwargs={0: vector_math_5.outputs["Vector"], 1: value_3},
attrs={"operation": "MULTIPLY"},
)
-
+
noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': position, "W": nw.new_value(uniform(0, 10), "noise_texture_3_w"), 'Scale': sample_ratio(5, 3/4, 4/3)},
attrs={"noise_dimensions": "4D"})
-
+
subtract = nw.new_node(Nodes.Math,
input_kwargs={0: noise_texture_3.outputs["Fac"]},
attrs={'operation': 'SUBTRACT'})
-
+
multiply_8 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: subtract, 1: normal},
attrs={'operation': 'MULTIPLY'})
-
+
value_5 = nw.new_node(Nodes.Value)
value_5.outputs[0].default_value = 0.05
-
+
multiply_9 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: multiply_8.outputs["Vector"], 1: value_5},
attrs={'operation': 'MULTIPLY'})
-
+
noise_texture_4 = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': position, 'Scale': sample_ratio(20, 3/4, 4/3), "W": nw.new_value(uniform(0, 10), "noise_texture_4_w")},
attrs={'noise_dimensions': '4D'})
-
+
colorramp_5 = nw.new_node(Nodes.ColorRamp,
input_kwargs={'Fac': noise_texture_4.outputs["Fac"]})
colorramp_5.color_ramp.elements.new(0)
@@ -204,22 +204,22 @@ def geo_dirt(nw, selection=None, random_seed=0, geometry=True):
colorramp_5.color_ramp.elements[2].color = (0.5, 0.5, 0.5, 1.0)
colorramp_5.color_ramp.elements[3].position = 1.0
colorramp_5.color_ramp.elements[3].color = (1.0, 1.0, 1.0, 1.0)
-
+
subtract_1 = nw.new_node(Nodes.Math,
input_kwargs={0: colorramp_5.outputs["Color"]},
attrs={'operation': 'SUBTRACT'})
-
+
multiply_10 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: subtract_1, 1: normal},
attrs={'operation': 'MULTIPLY'})
-
+
value_6 = nw.new_node(Nodes.Value)
value_6.outputs[0].default_value = 0.1
-
+
multiply_11 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: multiply_10.outputs["Vector"], 1: value_6},
attrs={'operation': 'MULTIPLY'})
-
+
colorramp = nw.new_node(
Nodes.ColorRamp, input_kwargs={"Fac": noise_texture.outputs["Fac"]}
)
@@ -232,7 +232,7 @@ def geo_dirt(nw, selection=None, random_seed=0, geometry=True):
colorramp.color_ramp.elements[2].color = (0.19, 0.03, 0.02, 1.0)
sample_color(colorramp.color_ramp.elements[1].color, offset=0.05)
sample_color(colorramp.color_ramp.elements[2].color, offset=0.05)
-
+
dirt_base_color = nw.new_node(
Nodes.MixRGB,
input_kwargs={
@@ -249,12 +249,12 @@ def geo_dirt(nw, selection=None, random_seed=0, geometry=True):
colorramp_3.color_ramp.elements[0].color = (0.0, 0.0, 0.0, 1.0)
colorramp_3.color_ramp.elements[1].position = 0.768
colorramp_3.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
-
+
dirt_roughness = colorramp_3
offset = nw.add(multiply_11, multiply_9, vector_math_8)
- if geometry:
+ if geometry:
noise_params = {"scale": ("uniform", 1, 5), "detail": 7, "roughness": 0.7, "zscale": ("power_uniform", -1, -0.5)}
offset = nw.add(
geo_MOUNTAIN_general(nw, 3, noise_params, 0, {}, {}),
@@ -290,4 +290,3 @@ def apply(obj, selection=None, **kwargs):
bpy.context.scene.render.image_settings.file_format='JPEG'
bpy.ops.render.render(write_still=True)
bpy.ops.wm.save_as_mainfile(filepath=os.path.join('outputs', mat, 'landscape_surface_dev_dirt.blend'))
-
\ No newline at end of file
diff --git a/infinigen/assets/materials/dishwasher_shaders.py b/infinigen/assets/materials/dishwasher_shaders.py
new file mode 100644
index 000000000..8f2c5f7f8
--- /dev/null
+++ b/infinigen/assets/materials/dishwasher_shaders.py
@@ -0,0 +1,85 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+def default_shader(nw: NodeWrangler):
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF)
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+def shader_black_medal_002(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ anisotropic_bsdf = nw.new_node('ShaderNodeBsdfAnisotropic', input_kwargs={'Color': (0.0167, 0.0167, 0.0167, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': anisotropic_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_glass_002(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ glass_bsdf = nw.new_node(Nodes.GlassBSDF, input_kwargs={'IOR': 1.5000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_metal_002(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (1.0000, 1.0000, 80.0000)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 10.0000, 'Detail': 20.0000, 'Roughness': 0.0000, 'Distortion': 1.0000})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Color"]})
+ colorramp.color_ramp.elements[0].position = 0.0045
+ colorramp.color_ramp.elements[0].color = [0.2218, 0.1914, 0.2173, 1.0000]
+ colorramp.color_ramp.elements[1].position = 0.4432
+ colorramp.color_ramp.elements[1].color = [0.1678, 0.1300, 0.0929, 1.0000]
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': 2.0000, 'Detail': 0.0000})
+
+ colorramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture.outputs["Color"]})
+ colorramp_1.color_ramp.elements.new(0)
+ colorramp_1.color_ramp.elements[0].position = 0.0000
+ colorramp_1.color_ramp.elements[0].color = [0.5000, 0.5000, 0.5000, 1.0000]
+ colorramp_1.color_ramp.elements[1].position = 0.5000
+ colorramp_1.color_ramp.elements[1].color = [0.3433, 0.3062, 0.1380, 1.0000]
+ colorramp_1.color_ramp.elements[2].position = 1.0000
+ colorramp_1.color_ramp.elements[2].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"], 'Subsurface Color': (0.9456, 0.5597, 0.0681, 1.0000), 'Metallic': 1.0000, 'Roughness': colorramp_1.outputs["Color"]},
+ attrs={'subsurface_method': 'BURLEY'})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_white_metal_002(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (1.0000, 1.0000, 50.0000)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 20.0000, 'Detail': 20.0000, 'Distortion': 1.0000})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Color"]})
+ colorramp.color_ramp.elements[0].position = 0.2500
+ colorramp.color_ramp.elements[0].color = [0.5244, 0.5244, 0.5244, 1.0000]
+ colorramp.color_ramp.elements[1].position = 1.0000
+ colorramp.color_ramp.elements[1].color = [0.9698, 0.9698, 0.9698, 1.0000]
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"], 'Subsurface Color': (1.0000, 1.0000, 1.0000, 1.0000), 'Metallic': 1.0000, 'Specular': 1.0000, 'Roughness': 0.1000, 'Anisotropic': 0.9182, 'Sheen': 0.0455, 'Sheen Tint': 0.4948},
+ attrs={'subsurface_method': 'BURLEY'})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/fabrics.py b/infinigen/assets/materials/fabrics.py
new file mode 100644
index 000000000..feab191bb
--- /dev/null
+++ b/infinigen/assets/materials/fabrics.py
@@ -0,0 +1,10 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from infinigen.assets.materials import leather_and_fabrics
+from .leather_and_fabrics import *
+
+
+def apply(obj, selection=None, **kwargs):
+ leather_and_fabrics.apply(obj, selection=selection, **kwargs)
diff --git a/infinigen/assets/materials/glass.py b/infinigen/assets/materials/glass.py
new file mode 100644
index 000000000..be4c28cbb
--- /dev/null
+++ b/infinigen/assets/materials/glass.py
@@ -0,0 +1,48 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import colorsys
+
+from numpy.random import uniform
+
+import bpy
+
+from infinigen.core.util.color import hsv2rgba
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+
+from infinigen.core.util import blender as butil
+
+def shader_glass(nw: NodeWrangler, color=None, is_window=False, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+ if color is None:
+ color = get_glass_color(clear=False)
+
+ # TODO windows are currently planes so refract and dont unrefract. ideally we just fix the geometry
+ # warning: currently this IOR also accidentally just turns off reflections, the window plane is pretty much invisible.
+ ior = 1.5 if not is_window else 1.0
+
+ light_path = nw.new_node(Nodes.LightPath)
+
+ transparent_bsdf = nw.new_node(Nodes.TransparentBSDF)
+
+ shader = nw.new_node(Nodes.GlassBSDF, input_kwargs={'Roughness': 0.0200, 'IOR': ior})
+
+ if is_window:
+ shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': light_path.outputs["Is Camera Ray"], 1: transparent_bsdf, 2: shader})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': shader}, attrs={'is_active_output': True})
+
+def apply(obj, selection=None, clear=False, **kwargs):
+ color = get_glass_color(clear)
+ common.apply(obj, shader_glass, selection, color, **kwargs)
+
+def get_glass_color(clear):
+ if uniform(0, 1) < .5:
+ color = 1, 1, 1, 1
+ else:
+ color = hsv2rgba(uniform(0, 1), .01 if clear else uniform(.05, .25), 1)
+ return color
diff --git a/infinigen/assets/materials/glass_volume.py b/infinigen/assets/materials/glass_volume.py
new file mode 100644
index 000000000..84fdc059b
--- /dev/null
+++ b/infinigen/assets/materials/glass_volume.py
@@ -0,0 +1,31 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo, Lingjie Mei
+
+from numpy.random import uniform
+
+from infinigen.core.util.color import hsv2rgba
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+
+def shader_glass_volume(nw: NodeWrangler, color=None, density=100.0, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if color is None:
+ if uniform(0, 1) < .3:
+ color = 1, 1, 1, 1
+ else:
+ color = hsv2rgba(uniform(0, 1), uniform(.5, .9), uniform(.6, .9))
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Roughness': 0.0000, 'Transmission': 1.0000})
+
+ volume_absorption = nw.new_node('ShaderNodeVolumeAbsorption',
+ input_kwargs={'Color': color, 'Density': density})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': principled_bsdf, 'Volume': volume_absorption},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_glass_volume, selection, **kwargs)
diff --git a/infinigen/assets/materials/hardwood_floor.py b/infinigen/assets/materials/hardwood_floor.py
new file mode 100644
index 000000000..34ca16bb5
--- /dev/null
+++ b/infinigen/assets/materials/hardwood_floor.py
@@ -0,0 +1,46 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from . import common
+from .utils.surface_utils import perturb_coordinates
+from .table_materials import shader_wood
+from infinigen.assets.utils.object import new_plane
+from ...core.nodes import NodeWrangler, Nodes
+from ...core.util.random import log_uniform
+
+
+def shader_hardwood_floor(nw: NodeWrangler, rotation=None):
+ vec = nw.new_node(Nodes.Mapping, [nw.new_node(Nodes.TextureCoord).outputs["Object"]],
+ input_kwargs={'Rotation': rotation})
+ color, mortar = map(
+ nw.new_node(Nodes.BrickTexture, [vec, (0, 0, 0, 1), (1, 1, 1, 1), (0, 0, 0, uniform(.01, .02))],
+ input_kwargs={
+ 'Scale': 1,
+ 'Row Height': log_uniform(.06, .15),
+ 'Brick Width': log_uniform(.6, 1),
+ 'Mortar Size': uniform(.002, .002)
+ }).outputs.get, ['Color', 'Fac'])
+ location = nw.combine(color, color, color)
+ shader_wood(nw)
+ perturb_coordinates(nw, nw.find(Nodes.TextureCoord)[1], location, 0)
+ principled_bsdf = nw.find(Nodes.PrincipledBSDF)[0]
+ wood_color = nw.find_from(principled_bsdf.inputs[0])[0].from_socket
+ color = nw.new_node(Nodes.MixRGB, [mortar, wood_color, color])
+ nw.links.remove(nw.find_from(principled_bsdf.inputs[0])[0])
+ nw.connect_input(principled_bsdf.inputs[0], color)
+
+
+def apply(obj, selection=None, rotation=None, **kwargs):
+ if rotation is None:
+ rotation = (0,0,0) if uniform() < .1 else (0,0,np.pi / 2)
+ return common.apply(obj, shader_hardwood_floor, selection, rotation, **kwargs)
+
+
+def make_sphere():
+ obj = new_plane()
+ obj.rotation_euler[0] = np.pi / 2
+ return obj
diff --git a/infinigen/assets/materials/invisible_to_camera.py b/infinigen/assets/materials/invisible_to_camera.py
new file mode 100644
index 000000000..3c3e1723e
--- /dev/null
+++ b/infinigen/assets/materials/invisible_to_camera.py
@@ -0,0 +1,37 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+def shader_invisible(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ light_path = nw.new_node(Nodes.LightPath)
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Roughness': 0.7697})
+
+ transparent_bsdf = nw.new_node(Nodes.TransparentBSDF)
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': light_path.outputs["Is Camera Ray"], 1: principled_bsdf, 2: transparent_bsdf})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': mix_shader}, attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+
+ if not isinstance(obj, list):
+ obj = [obj]
+
+ for o in obj:
+ for i in range(len(o.material_slots)):
+ bpy.ops.object.material_slot_remove({'object': o})
+ surface.add_material(obj, shader_invisible, selection=selection)
\ No newline at end of file
diff --git a/infinigen/assets/materials/lamp_shaders.py b/infinigen/assets/materials/lamp_shaders.py
new file mode 100644
index 000000000..55f437b78
--- /dev/null
+++ b/infinigen/assets/materials/lamp_shaders.py
@@ -0,0 +1,59 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from numpy.random import uniform as U, normal as N, randint as RI
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+def shader_metal(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ anisotropic_bsdf = nw.new_node('ShaderNodeBsdfAnisotropic',
+ input_kwargs={'Color': (0.3224, 0.3224, 0.3224, 1.0000), 'Roughness': 0.1000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': anisotropic_bsdf}, attrs={'is_active_output': True})
+
+def shader_lampshade(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ light_path = nw.new_node(Nodes.LightPath)
+
+ object_info = nw.new_node(Nodes.ObjectInfo_Shader)
+
+ white_noise_texture = nw.new_node(Nodes.WhiteNoiseTexture,
+ input_kwargs={'Vector': object_info.outputs["Random"]},
+ attrs={'noise_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: white_noise_texture.outputs["Color"], 7: (0.5000, 0.4444, 0.3669, 1.0000)},
+ attrs={'data_type': 'RGBA'})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={
+ 'Base Color': mix.outputs[2],
+ 'Subsurface': U(0.03, 0.08),
+ 'Subsurface Radius': (0.1000, 0.1000, 0.1000),
+ 'Subsurface IOR': 1.6029,
+ 'Roughness': U(0.5, 0.8),
+ 'IOR': 4.0000,
+ 'Transmission': U(0.05, 0.2),
+ 'Transmission Roughness': 1.0000
+ }
+ )
+
+ translucent_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': mix.outputs[2], 'Roughness': 0.7, })
+
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': light_path.outputs["Is Camera Ray"], 1: principled_bsdf, 2: translucent_bsdf})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': mix_shader}, attrs={'is_active_output': True})
+
+
+def shader_black(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': (0.0039, 0.0039, 0.0039, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/leather_and_fabrics/__init__.py b/infinigen/assets/materials/leather_and_fabrics/__init__.py
new file mode 100644
index 000000000..751f12282
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .general_fabric import shader_fabric
+from .lined_fabric import shader_lined_fur_base
+from .coarse_knit_fabric import shader_fabric_random as shader_coarse_fabric_random
+from .fine_knit_fabric import shader_fabric_random as shader_fine_fabric_random
+from .leather import shader_leather
+from .sofa_fabric import shader_sofa_fabric
+
+from infinigen.core.util.random import random_general as rg
+from .. import common
+from ...utils.uv import unwrap_faces
+
+fabric_shader_list = 'weighted_choice', (1, shader_coarse_fabric_random), (1, shader_fine_fabric_random), \
+ (2, shader_leather), (1, shader_sofa_fabric), # (1, shader_fabric),
+
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, rg(fabric_shader_list), selection=selection, **kwargs)
diff --git a/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py b/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py
new file mode 100644
index 000000000..0ac7179b0
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py
@@ -0,0 +1,270 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+# Acknowledgement: This file draws inspiration from following sources:
+
+# https://www.youtube.com/watch?v=DfoMWLQ-BkM by 5 Minutes Blender
+# https://www.youtube.com/watch?v=tS_U3twxKKg by PIXXO 3D
+# https://www.youtube.com/watch?v=OCay8AsVD84 by Antonio Palladino
+# https://www.youtube.com/watch?v=5dS3N90wPkc by Dr Blender
+# https://www.youtube.com/watch?v=12c1J6LhK4Y by blenderian
+# https://www.youtube.com/watch?v=kVvOk_7PoUE by Blender Box
+# https://www.youtube.com/watch?v=WTK7E443l1E by blenderbitesize
+# https://www.youtube.com/watch?v=umrARvXC_MI by Ryan King Art
+
+
+import bpy
+import mathutils
+from numpy.random import uniform, normal, choice
+
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.materials import common
+
+
+def get_texture_params():
+ return {
+ "_pattern_mixer": choice([uniform(0.0, 0.75), uniform(0.75, 1.0)]),
+ "_pattern_density": choice([uniform(0.1, 1.0), uniform(1.0, 10.0)]),
+ "_color": uniform(0.0, 1.0, 3),
+ "_brick_knit": choice(
+ [uniform(0.0, 0.05), uniform(0.05, 0.95), uniform(0.95, 1.0)]
+ ),
+ "_knit_resolution": uniform(0.5, 3.0),
+ "_brick_resolution": uniform(10.0, 30.0),
+ "_crease_resolution": uniform(10.0, 80.0),
+ "_smoothness": choice([uniform(0.0, 0.2), uniform(0.2, 1.0)]),
+ "_color_shader_frac": uniform(0.1, 0.9),
+ }
+
+
+def shader_fabric_base(
+ nw: NodeWrangler,
+ _pattern_mixer=1.0,
+ _pattern_density=0.15,
+ _color=[5, 10.0, 10.0],
+ _brick_knit=0.0,
+ _knit_resolution=3.0,
+ _brick_resolution=40,
+ _crease_resolution=200,
+ _smoothness=0.7,
+ _color_shader_frac=0.01,
+):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ pattern_mixer = nw.new_node(Nodes.Value)
+ pattern_mixer.outputs[0].default_value = _pattern_mixer
+
+ pattern_density = nw.new_node(Nodes.Value)
+ pattern_density.outputs[0].default_value = _pattern_density
+
+ color_r = nw.new_node(Nodes.Value)
+ color_r.outputs[0].default_value = _color[0]
+
+ color_g = nw.new_node(Nodes.Value)
+ color_g.outputs[0].default_value = _color[1]
+
+ color_b = nw.new_node(Nodes.Value)
+ color_b.outputs[0].default_value = _color[2]
+
+ brick_knit = nw.new_node(Nodes.Value)
+ brick_knit.outputs[0].default_value = _brick_knit
+
+ knit_resolution = nw.new_node(Nodes.Value)
+ knit_resolution.outputs[0].default_value = _knit_resolution
+
+ brick_resolution = nw.new_node(Nodes.Value)
+ brick_resolution.outputs[0].default_value = _brick_resolution
+
+ crease_resolution = nw.new_node(Nodes.Value)
+ crease_resolution.outputs[0].default_value = _crease_resolution
+
+ smoothness = nw.new_node(Nodes.Value)
+ smoothness.outputs[0].default_value = _smoothness
+
+ color_shader_frac = nw.new_node(Nodes.Value)
+ color_shader_frac.outputs[0].default_value = _color_shader_frac
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping_1 = nw.new_node(
+ Nodes.Mapping,
+ input_kwargs={
+ "Vector": texture_coordinate.outputs["Object"],
+ "Scale": (3.2000, 1.0000, 1.0000),
+ },
+ )
+
+ brick_texture = nw.new_node(
+ Nodes.BrickTexture,
+ input_kwargs={"Vector": mapping_1, "Scale": brick_resolution},
+ )
+
+ color_ramp_1 = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": brick_texture.outputs["Color"]}
+ )
+ color_ramp_1.color_ramp.elements[0].position = 0.0000
+ color_ramp_1.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp_1.color_ramp.elements[1].position = 1.0000
+ color_ramp_1.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ mapping = nw.new_node(
+ Nodes.Mapping,
+ input_kwargs={
+ "Vector": texture_coordinate.outputs["Object"],
+ "Rotation": (0.0000, 0.0000, 0.7854),
+ "Scale": (238.8000, 1.0000, 35.6000),
+ },
+ )
+
+ voronoi_texture = nw.new_node(
+ Nodes.VoronoiTexture,
+ input_kwargs={
+ "Vector": mapping,
+ "Scale": pattern_density,
+ "Randomness": 0.0000,
+ },
+ attrs={"feature": "F2"},
+ )
+
+ color_ramp = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": voronoi_texture.outputs["Distance"]}
+ )
+ color_ramp.color_ramp.elements[0].position = 0.1018
+ color_ramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 1.0000
+ color_ramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ mix = nw.new_node(
+ Nodes.Mix,
+ input_kwargs={
+ 0: pattern_mixer,
+ 6: color_ramp_1.outputs["Color"],
+ 7: color_ramp.outputs["Color"],
+ },
+ attrs={"clamp_result": True, "data_type": "RGBA"},
+ )
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={"Input": mix.outputs[2]})
+
+ principled_bsdf = nw.new_node(
+ Nodes.PrincipledBSDF,
+ input_kwargs={"Base Color": reroute, "Specular": 0.6309, "Roughness": 0.9945},
+ )
+
+ combine_color = nw.new_node(
+ Nodes.CombineColor,
+ input_kwargs={"Red": color_r, "Green": color_g, "Blue": color_b},
+ )
+
+ principled_bsdf_1 = nw.new_node(
+ Nodes.PrincipledBSDF,
+ input_kwargs={
+ "Base Color": combine_color,
+ "Specular": 0.6309,
+ "Roughness": 0.9945,
+ },
+ )
+
+ mix_shader = nw.new_node(
+ Nodes.MixShader,
+ input_kwargs={
+ "Fac": color_shader_frac,
+ 1: principled_bsdf,
+ 2: principled_bsdf_1,
+ },
+ )
+
+ # bump_1 = nw.new_node(Nodes.Bump, input_kwargs={'Height': color_ramp_1.outputs["Color"]})
+
+ scale = nw.new_node(
+ Nodes.VectorMath,
+ input_kwargs={0: color_ramp_1.outputs["Color"], "Scale": brick_knit},
+ attrs={"operation": "SCALE"},
+ )
+
+ mapping_2 = nw.new_node(
+ Nodes.Mapping,
+ input_kwargs={
+ "Vector": texture_coordinate.outputs["Object"],
+ "Rotation": (0.0000, 0.0000, 0.6196),
+ "Scale": (217.5000, 176.2000, 42.0000),
+ },
+ )
+
+ voronoi_texture_1 = nw.new_node(
+ Nodes.VoronoiTexture,
+ input_kwargs={
+ "Vector": mapping_2,
+ "Scale": knit_resolution,
+ "Randomness": 0.0000,
+ },
+ attrs={"feature": "F2"},
+ )
+
+ mapping_3 = nw.new_node(
+ Nodes.Mapping, input_kwargs={"Vector": texture_coordinate.outputs["Object"]}
+ )
+
+ noise_texture = nw.new_node(
+ Nodes.NoiseTexture,
+ input_kwargs={"Vector": mapping_3, "Scale": crease_resolution},
+ )
+
+ add = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: noise_texture.outputs["Fac"], 1: smoothness},
+ attrs={"use_clamp": True},
+ )
+
+ multiply = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: voronoi_texture_1.outputs["Distance"], 1: add},
+ attrs={"use_clamp": True, "operation": "MULTIPLY"},
+ )
+
+ # bump = nw.new_node(Nodes.Bump, input_kwargs={'Height': multiply})
+
+ subtract = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: 1.0000, 1: brick_knit},
+ attrs={"operation": "SUBTRACT"},
+ )
+
+ scale_1 = nw.new_node(
+ Nodes.VectorMath,
+ input_kwargs={0: multiply, "Scale": subtract},
+ attrs={"operation": "SCALE"},
+ )
+
+ # scale_1 = nw.new_node(Nodes.VectorMath, input_kwargs={0: bump, 'Scale': subtract}, attrs={'operation': 'SCALE'})
+
+ add_1 = nw.new_node(
+ Nodes.VectorMath,
+ input_kwargs={0: scale.outputs["Vector"], 1: scale_1.outputs["Vector"]},
+ )
+
+ vector_displacement = nw.new_node(
+ "ShaderNodeVectorDisplacement", input_kwargs={"Vector": add_1.outputs["Vector"]}
+ )
+
+ material_output = nw.new_node(
+ Nodes.MaterialOutput,
+ input_kwargs={"Surface": mix_shader, "Displacement": vector_displacement},
+ attrs={"is_active_output": True},
+ )
+
+
+def shader_fabric_random(nw: NodeWrangler, **kwargs):
+ fabric_params = get_texture_params()
+ return shader_fabric_base(nw, **fabric_params)
+
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, shader_fabric_random, selection, **kwargs)
diff --git a/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py b/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py
new file mode 100644
index 000000000..d3d8f2dcf
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py
@@ -0,0 +1,154 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+# Acknowledgement: This file draws inspiration from following sources:
+
+# https://www.youtube.com/watch?v=DfoMWLQ-BkM by 5 Minutes Blender
+# https://www.youtube.com/watch?v=tS_U3twxKKg by PIXXO 3D
+# https://www.youtube.com/watch?v=OCay8AsVD84 by Antonio Palladino
+# https://www.youtube.com/watch?v=5dS3N90wPkc by Dr Blender
+# https://www.youtube.com/watch?v=12c1J6LhK4Y by blenderian
+# https://www.youtube.com/watch?v=kVvOk_7PoUE by Blender Box
+# https://www.youtube.com/watch?v=WTK7E443l1E by blenderbitesize
+# https://www.youtube.com/watch?v=umrARvXC_MI by Ryan King Art
+
+
+import bpy
+from numpy.random import uniform
+
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.assets.materials import common
+
+
+def get_texture_params():
+ return {
+ "_color": uniform(0.2, 1.0, 3),
+ "_roughness": uniform(0, 1.0),
+ "_thread_density_x": uniform(100, 300),
+ "_relative_density_y": uniform(0.75, 1.33),
+ }
+
+
+def shader_material(
+ nw: NodeWrangler,
+ _color=[0.2, 0.2, 0.2],
+ _roughness=0.4,
+ _thread_density_x=200,
+ _relative_density_y=1.0,
+ _map="UV",
+):
+ red = nw.new_node(Nodes.Value, label="red")
+ red.outputs[0].default_value = _color[0]
+
+ green = nw.new_node(Nodes.Value, label="green")
+ green.outputs[0].default_value = _color[1]
+
+ blue = nw.new_node(Nodes.Value, label="blue")
+ blue.outputs[0].default_value = _color[2]
+
+ roughness = nw.new_node(Nodes.Value, label="roughness")
+ roughness.outputs[0].default_value = _roughness
+
+ thread_density_x = nw.new_node(Nodes.Value, label="thread_density_x")
+ thread_density_x.outputs[0].default_value = _thread_density_x
+
+ relative_density_y = nw.new_node(Nodes.Value, label="relative_density_y")
+ relative_density_y.outputs[0].default_value = _relative_density_y
+
+ combine_color = nw.new_node(
+ Nodes.CombineColor, input_kwargs={"Red": red, "Green": green, "Blue": blue}
+ )
+ principled_bsdf = nw.new_node(
+ Nodes.PrincipledBSDF,
+ input_kwargs={"Base Color": combine_color, "Roughness": roughness},
+ )
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ reroute = nw.new_node(
+ Nodes.Reroute, input_kwargs={"Input": texture_coordinate.outputs[_map]}
+ )
+
+ multiply = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: thread_density_x, 1: relative_density_y},
+ attrs={"operation": "MULTIPLY"},
+ )
+
+ wave_texture_1 = nw.new_node(
+ Nodes.WaveTexture,
+ input_kwargs={
+ "Vector": reroute,
+ "Scale": multiply,
+ "Distortion": 5.0000,
+ "Detail": 6.1000,
+ },
+ attrs={"bands_direction": "Y"},
+ )
+
+ principled_bsdf_1 = nw.new_node(
+ Nodes.PrincipledBSDF,
+ input_kwargs={
+ "Base Color": wave_texture_1.outputs["Color"],
+ "Subsurface Color": (0.0000, 0.0000, 0.0000, 1.0000),
+ "Roughness": roughness,
+ },
+ )
+
+ mix_shader = nw.new_node(
+ Nodes.MixShader,
+ input_kwargs={"Fac": 0.1333, 1: principled_bsdf, 2: principled_bsdf_1},
+ )
+
+ wave_texture = nw.new_node(
+ Nodes.WaveTexture,
+ input_kwargs={
+ "Vector": reroute,
+ "Scale": thread_density_x,
+ "Distortion": 3.8000,
+ "Detail": 6.1000,
+ },
+ )
+
+ color_ramp = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": wave_texture.outputs["Color"]}
+ )
+ color_ramp.color_ramp.elements[0].position = 0.8109
+ color_ramp.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 1.0000
+ color_ramp.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ invert_color = nw.new_node(
+ Nodes.Invert, input_kwargs={"Fac": 0.8400, "Color": color_ramp.outputs["Color"]}
+ )
+
+ color_ramp_1 = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": wave_texture_1.outputs["Color"]}
+ )
+ color_ramp_1.color_ramp.elements[0].position = 0.0727
+ color_ramp_1.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp_1.color_ramp.elements[1].position = 0.8655
+ color_ramp_1.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ add = nw.new_node(
+ Nodes.Math, input_kwargs={0: invert_color, 1: color_ramp_1.outputs["Color"]}
+ )
+
+ material_output = nw.new_node(
+ Nodes.MaterialOutput,
+ input_kwargs={"Surface": mix_shader, "Displacement": add},
+ attrs={"is_active_output": True},
+ )
+
+
+def shader_fabric_random(nw: NodeWrangler, **kwargs):
+ fabric_params = get_texture_params()
+ fabric_params["_map"] = "Object"
+ return shader_material(nw, **fabric_params)
+
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, shader_fabric_random, selection, **kwargs)
diff --git a/infinigen/assets/materials/leather_and_fabrics/general_fabric.py b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py
new file mode 100644
index 000000000..33e1e987e
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py
@@ -0,0 +1,170 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=umrARvXC_MI by Ryan King Art
+
+
+from infinigen.assets.materials import common
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.utils.uv import ensure_uv, unwrap_faces
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+def func_fabric(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ group_input = {
+ 'Weave Scale': 0.,
+ 'Color Pattern Scale': 0.,
+ 'Color1': (0.7991, 0.1046, 0.1195, 1.0000),
+ 'Color2': (1.0000, 0.5271, 0.5711, 1.0000)
+ }
+ group_input.update(kwargs)
+
+ wave_texture_1 = nw.new_node(Nodes.WaveTexture, input_kwargs={
+ 'Vector': texture_coordinate.outputs["UV"],
+ 'Scale': group_input["Weave Scale"],
+ 'Distortion': 7.0000,
+ 'Detail': 15.0000
+ }, attrs={'bands_direction': 'Y'})
+
+ value_2 = nw.new_node(Nodes.Value)
+ value_2.outputs[0].default_value = 0.1000
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': wave_texture_1.outputs["Color"], 1: value_2})
+
+ wave_texture = nw.new_node(Nodes.WaveTexture, input_kwargs={
+ 'Vector': texture_coordinate.outputs["UV"],
+ 'Scale': group_input["Weave Scale"],
+ 'Distortion': 7.0000,
+ 'Detail': 15.0000
+ })
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': wave_texture.outputs["Color"], 1: value_2})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: map_range.outputs["Result"], 7: map_range_1.outputs["Result"]},
+ attrs={'data_type': 'RGBA'})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: mix.outputs[2], 1: 0.1000},
+ attrs={'operation': 'GREATER_THAN'})
+
+ transparent_bsdf = nw.new_node(Nodes.TransparentBSDF)
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: group_input["Color Pattern Scale"], 1: 0.0001},
+ attrs={'operation': 'LESS_THAN'})
+
+ brick_texture_2 = nw.new_node(Nodes.BrickTexture, input_kwargs={
+ 'Vector': texture_coordinate.outputs["UV"],
+ 'Color1': group_input["Color1"],
+ 'Mortar': group_input["Color2"],
+ 'Scale': group_input["Color Pattern Scale"],
+ 'Mortar Size': 0.0000,
+ 'Bias': -1.0000,
+ 'Row Height': 0.5000
+ }, attrs={'offset_frequency': 1, 'squash': 0.0000})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate, input_kwargs={
+ 'Vector': texture_coordinate.outputs["UV"],
+ 'Rotation': (0.0000, 0.0000, 1.5708)
+ }, attrs={'rotation_type': 'EULER_XYZ'})
+
+ brick_texture = nw.new_node(Nodes.BrickTexture, input_kwargs={
+ 'Vector': vector_rotate,
+ 'Color1': group_input["Color1"],
+ 'Mortar': group_input["Color2"],
+ 'Scale': group_input["Color Pattern Scale"],
+ 'Mortar Size': 0.0000,
+ 'Bias': -1.0000,
+ 'Row Height': 0.5000
+ }, attrs={'offset_frequency': 1, 'squash': 0.0000})
+
+ mix_2 = nw.new_node(Nodes.Mix, input_kwargs={
+ 0: 1.0000,
+ 6: brick_texture_2.outputs["Color"],
+ 7: brick_texture.outputs["Color"]
+ }, attrs={'data_type': 'RGBA', 'blend_type': 'ADD'})
+
+ mix_4 = nw.new_node(Nodes.Mix, input_kwargs={0: less_than, 6: mix_2.outputs[2], 7: group_input["Color1"]},
+ attrs={'data_type': 'RGBA'})
+
+ mix_3 = nw.new_node(Nodes.Mix, input_kwargs={
+ 0: mix.outputs[2],
+ 6: (0.0000, 0.0000, 0.0000, 1.0000),
+ 7: mix_4.outputs[2]
+ }, attrs={'data_type': 'RGBA'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': mix.outputs[2], 3: 1.0000, 4: 0.9000})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': mix_3.outputs[2],
+ 'Roughness': map_range_2.outputs["Result"],
+ 'Sheen': 1.0000,
+ 'Sheen Tint': 1.0000
+ })
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': greater_than, 1: transparent_bsdf, 2: principled_bsdf})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input["Weave Scale"], 1: 5.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture, input_kwargs={'Scale': multiply})
+
+ mix_1 = nw.new_node(Nodes.Mix, input_kwargs={6: musgrave_texture, 7: mix.outputs[2]},
+ attrs={'data_type': 'RGBA'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: mix_1.outputs[2]}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: 0.0010}, attrs={'operation': 'MULTIPLY'})
+
+ displacement = nw.new_node(
+ 'ShaderNodeDisplacement', input_kwargs={'Height': multiply_1, 'Midlevel': 0.0000}
+ )
+
+ return {'Shader': mix_shader, 'Displacement': displacement}
+
+
+def shader_fabric(nw: NodeWrangler, weave_scale=500.0, color_scale=None, color_1=None, color_2=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ if color_scale is None:
+ color_scale = np.random.choice([0.0, uniform(5., 20.)])
+ if color_1 is None:
+ color_1 = color_category('fabric')
+ if color_2 is None:
+ color_2 = color_category('white')
+
+ group = func_fabric(nw, **{
+ 'Weave Scale': weave_scale,
+ 'Color Pattern Scale': color_scale,
+ 'Color1': color_1,
+ 'Color2': color_2
+ })
+
+ displacement = nw.new_node('ShaderNodeDisplacement',
+ input_kwargs={'Height': group["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group["Shader"], 'Displacement': displacement
+ }, attrs={'is_active_output': True})
+
+
+def apply(obj, selection=None, **kwargs):
+ if not isinstance(obj, list):
+ obj = [obj]
+ for o in obj:
+ unwrap_faces(o, selection)
+ common.apply(obj, shader_fabric, selection, **kwargs)
diff --git a/infinigen/assets/materials/leather_and_fabrics/leather.py b/infinigen/assets/materials/leather_and_fabrics/leather.py
new file mode 100644
index 000000000..808f4a950
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/leather.py
@@ -0,0 +1,110 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=In9V4-ih16o by Ryan King Art
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+import functools
+
+from infinigen.assets.materials import common
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+from infinigen.assets.color_fits import real_color_distribution
+
+@node_utils.to_nodegroup('nodegroup_leather', singleton=False, type='ShaderNodeTree')
+def nodegroup_leather(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketColor', 'Base Color', (0.0000, 0.0000, 0.0000, 1.0000))])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 10.0000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Distortion': 0.2000},
+ attrs={'noise_dimensions': '4D'})
+
+ color_ramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture.outputs["Fac"]})
+ color_ramp.color_ramp.elements[0].position = 0.2841
+ color_ramp.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 0.9455
+ color_ramp.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.0200, 6: texture_coordinate.outputs["Object"], 7: noise_texture.outputs["Color"]},
+ attrs={'blend_type': 'LINEAR_LIGHT', 'data_type': 'RGBA'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 800.0000}, attrs={'operation': 'MULTIPLY'})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mix.outputs[2], 'W': group_input.outputs["Seed"], 'Scale': multiply_1},
+ attrs={'voronoi_dimensions': '4D', 'feature': 'DISTANCE_TO_EDGE'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: voronoi_texture.outputs["Distance"], 1: group_input.outputs["Scale"]},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ hue_saturation_value = nw.new_node('ShaderNodeHueSaturation', input_kwargs={'Value': 0.6000, 'Color': group_input.outputs["Base Color"]})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: multiply_2, 6: group_input.outputs["Base Color"], 7: hue_saturation_value},
+ attrs={'data_type': 'RGBA'})
+
+ hue_saturation_value_1 = nw.new_node('ShaderNodeHueSaturation', input_kwargs={'Value': 0.4000, 'Color': group_input.outputs["Base Color"]})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: color_ramp.outputs["Color"], 6: mix_1.outputs[2], 7: hue_saturation_value_1},
+ attrs={'data_type': 'RGBA'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': mix_2.outputs[2], 3: uniform(.3, .5), 4: uniform(.5, .7)})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': mix_2.outputs[2], 'Roughness': map_range.outputs["Result"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: mix_1.outputs[2], 1: -0.2000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: color_ramp.outputs["Color"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_3, 1: multiply_4})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 0.0200}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': multiply_5},
+ attrs={'is_active_output': True})
+
+def shader_leather(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+
+ # if base_color is None:
+ # base_color = color_category('leather')
+ base_color = real_color_distribution('sofa_leather')
+
+ group = nw.new_node(nodegroup_leather().name,
+ input_kwargs={'Seed': seed, 'Scale': scale, 'Base Color': base_color})
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, shader_leather, selection=selection, **kwargs)
+
diff --git a/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py b/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py
new file mode 100644
index 000000000..751d52479
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py
@@ -0,0 +1,167 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+
+
+import bpy
+import mathutils
+from numpy.random import uniform
+
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.assets.materials import common
+
+
+def get_texture_params():
+ return {
+ "_hue": uniform(0, 1.0),
+ "_saturation": uniform(0.5, 1.0),
+ "_line_density": uniform(5, 20.0),
+ }
+
+
+def shader_lined_fur_base(
+ nw: NodeWrangler, _hue=0.3, _saturation=0.7, _line_density=10
+):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ hue = nw.new_node(Nodes.Value)
+ hue.outputs[0].default_value = _hue
+
+ saturation = nw.new_node(Nodes.Value)
+ saturation.outputs[0].default_value = _saturation
+
+ line_density = nw.new_node(Nodes.Value)
+ line_density.outputs[0].default_value = _line_density
+
+ combine_color = nw.new_node(
+ Nodes.CombineColor,
+ input_kwargs={"Red": hue, "Green": saturation, "Blue": 0.6000},
+ attrs={"mode": "HSV"},
+ )
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(
+ Nodes.Mapping, input_kwargs={"Vector": texture_coordinate.outputs["Object"]}
+ )
+
+ wave_texture = nw.new_node(
+ Nodes.WaveTexture,
+ input_kwargs={
+ "Vector": mapping,
+ "Scale": line_density,
+ "Distortion": 1.0000,
+ "Detail": 1.0000,
+ },
+ )
+
+ color_ramp_1 = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": wave_texture.outputs["Color"]}
+ )
+ color_ramp_1.color_ramp.elements[0].position = 0.0073
+ color_ramp_1.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp_1.color_ramp.elements[1].position = 0.2255
+ color_ramp_1.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ mapping_1 = nw.new_node(
+ Nodes.Mapping,
+ input_kwargs={
+ "Vector": texture_coordinate.outputs["Object"],
+ "Scale": (1.0000, 1.0000, 87.4000),
+ },
+ )
+
+ noise_texture = nw.new_node(
+ Nodes.NoiseTexture,
+ input_kwargs={
+ "Vector": mapping_1,
+ "Scale": 2.7000,
+ "Detail": 7.3000,
+ "Distortion": 7.0000,
+ },
+ )
+
+ color_ramp = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": noise_texture.outputs["Fac"]}
+ )
+ color_ramp.color_ramp.elements[0].position = 0.3018
+ color_ramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 0.4691
+ color_ramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: color_ramp_1.outputs["Color"], 1: color_ramp.outputs["Color"]},
+ attrs={"operation": "MULTIPLY", "use_clamp": True},
+ )
+
+ scale = nw.new_node(
+ Nodes.VectorMath,
+ input_kwargs={0: combine_color, "Scale": multiply},
+ attrs={"operation": "SCALE"},
+ )
+
+ principled_bsdf = nw.new_node(
+ Nodes.PrincipledBSDF,
+ input_kwargs={"Base Color": scale.outputs["Vector"], "Roughness": 1.0000},
+ )
+
+ subtract = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: 1.0000, 1: multiply},
+ attrs={"operation": "SUBTRACT"},
+ )
+
+ multiply_1 = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: subtract, 1: -0.3000},
+ attrs={"operation": "MULTIPLY"},
+ )
+
+ mapping_2 = nw.new_node(
+ Nodes.Mapping, input_kwargs={"Vector": texture_coordinate.outputs["Object"]}
+ )
+
+ noise_texture_1 = nw.new_node(
+ Nodes.NoiseTexture,
+ input_kwargs={
+ "Vector": mapping_2,
+ "Scale": 42.3000,
+ "Detail": 7.3000,
+ "Distortion": 16.6000,
+ },
+ )
+
+ color_ramp_2 = nw.new_node(
+ Nodes.ColorRamp, input_kwargs={"Fac": noise_texture_1.outputs["Fac"]}
+ )
+ color_ramp_2.color_ramp.elements[0].position = 0.3018
+ color_ramp_2.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp_2.color_ramp.elements[1].position = 0.4691
+ color_ramp_2.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply_2 = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: color_ramp_2.outputs["Color"], 1: multiply},
+ attrs={"operation": "MULTIPLY", "use_clamp": True},
+ )
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: multiply_2})
+
+ material_output = nw.new_node(
+ Nodes.MaterialOutput,
+ input_kwargs={"Surface": principled_bsdf, "Displacement": add},
+ attrs={"is_active_output": True},
+ )
+
+
+def shader_fabric_random(nw: NodeWrangler, **kwargs):
+ fabric_params = get_texture_params()
+ return shader_lined_fur_base(nw, **fabric_params)
+
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, shader_fabric_random, selection, **kwargs)
diff --git a/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py b/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py
new file mode 100644
index 000000000..03292592a
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py
@@ -0,0 +1,40 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from numpy.random import uniform
+
+from infinigen.assets.materials import common
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.color import color_category
+
+
+def shader_sofa_fabric(nw: NodeWrangler, scale=1, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ attribute = nw.new_node(Nodes.Attribute, attrs={'attribute_name': 'UVMap'})
+ attribute = nw.new_node(Nodes.Mapping,[attribute],input_kwargs={'Scale':[scale]*3})
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = color_category('fabric')
+
+ brightness_contrast = nw.new_node('ShaderNodeBrightContrast', input_kwargs={'Color': rgb, 'Bright': uniform(-0.1500, -0.05)})
+
+ brick_texture = nw.new_node(Nodes.BrickTexture,
+ input_kwargs={'Vector': attribute.outputs["Vector"], 'Color1': rgb, 'Color2': brightness_contrast, 'Scale': 276.9800, 'Mortar Size': 0.0100, 'Mortar Smooth': 1.0000, 'Bias': 0.5000, 'Row Height': 0.1000},
+ attrs={'offset': 0.5479, 'squash_frequency': 1})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': brick_texture.outputs["Color"], 'Roughness': 0.8624, 'Sheen': 1.0000})
+
+ displacement = nw.new_node(Nodes.Displacement, input_kwargs={'Height': brick_texture.outputs["Fac"]})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ unwrap_faces(obj, selection)
+ common.apply(obj, shader_sofa_fabric, selection, **kwargs)
+
diff --git a/infinigen/assets/materials/leather_and_fabrics/velvet.py b/infinigen/assets/materials/leather_and_fabrics/velvet.py
new file mode 100644
index 000000000..3f7f8e2a4
--- /dev/null
+++ b/infinigen/assets/materials/leather_and_fabrics/velvet.py
@@ -0,0 +1,86 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Stamatis Alexandropoulos
+# Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=55MMAnTYhWI by Dikko
+
+import bpy
+import mathutils
+from infinigen.assets.materials import common
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+
+def shader_velvet(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': texture_coordinate.outputs["Object"]})
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': reroute})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture, input_kwargs={'Vector': mapping, 'Scale': 1.0000})
+
+ mix_6 = nw.new_node(Nodes.Mix, input_kwargs={0: 0.1125, 6: voronoi_texture.outputs["Color"]}, attrs={'data_type': 'RGBA'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 9.6000, 'Detail': 11.4000, 'Dimension': 0.1000, 'Lacunarity': 1.9000},
+ attrs={'musgrave_type': 'MULTIFRACTAL'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: uniform(0,0.8), 6: musgrave_texture, 7: (0.6044, 0.6044, 0.6044, 1.0000)},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ mix_1 = nw.new_node(Nodes.Mix, input_kwargs={6: mix_6.outputs[2], 7: mix.outputs[2]}, attrs={'data_type': 'RGBA'})
+
+ color_ramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_1.outputs[2]})
+ color_ramp.color_ramp.elements[0].position = 0.0000
+ color_ramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 0.8455
+ color_ramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = color_category('textile')
+ # (0.3547, 0.3018, 0.3087, 1.0000)
+
+ brightness_contrast = nw.new_node('ShaderNodeBrightContrast', input_kwargs={'Color': rgb, 'Bright': 0.0500})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: color_ramp.outputs["Color"], 6: brightness_contrast, 7: rgb},
+ attrs={'data_type': 'RGBA'})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': mix_2.outputs[2], 'Specular': 0.0000, 'Roughness': uniform(0.4,0.9), 'Anisotropic': 0.7614, 'Anisotropic Rotation': 1.0000, 'Sheen': 16.2273, 'Sheen Tint': 1.0000})
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': reroute, 'Rotation': (0.0000, 0.0000, 1.0157), 'Scale': (2.2000, 2.2000, 2.2000)})
+
+ wave_texture_1 = nw.new_node(Nodes.WaveTexture,
+ input_kwargs={'Vector': mapping_1, 'Scale': 500.0000, 'Distortion': 4.0000, 'Detail': 6.7000, 'Detail Scale': 1.5000, 'Detail Roughness': 0.4308},
+ attrs={'bands_direction': 'DIAGONAL'})
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: mapping_1, 7: wave_texture_1.outputs["Color"]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ mix_4 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: color_ramp.outputs["Color"], 7: mix_3.outputs[2]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ displacement = nw.new_node(Nodes.Displacement, input_kwargs={'Height': mix_4.outputs[2], 'Midlevel': 0.0000, 'Scale': 0.0150})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_velvet, selection, **kwargs)
+ # surface.add_material(obj, shader_velvet, selection=selection)
+# apply(bpy.context.active_object)
\ No newline at end of file
diff --git a/infinigen/assets/materials/marble.py b/infinigen/assets/materials/marble.py
new file mode 100644
index 000000000..51c23644a
--- /dev/null
+++ b/infinigen/assets/materials/marble.py
@@ -0,0 +1,10 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from infinigen.assets.materials import common
+from infinigen.assets.materials.table_materials import shader_marble
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_marble, selection, **kwargs)
diff --git a/infinigen/assets/materials/marble_regular.py b/infinigen/assets/materials/marble_regular.py
new file mode 100644
index 000000000..1da008c3c
--- /dev/null
+++ b/infinigen/assets/materials/marble_regular.py
@@ -0,0 +1,58 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Zeyu Ma
+# Acknowledgement: This file draws inspiration from https://physbam.stanford.edu/cs448x/old/Procedural_Noise(2f)Perlin_Noise.html
+
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core import surface
+
+
+
+def shader_material_001(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ geometry = nw.new_node(Nodes.NewGeometry)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': geometry.outputs["Position"], 'Scale': (20.0000, 20.0000, 20.0000)})
+
+ roughness = nw.new_node(Nodes.Value, label='roughness ~ U(0.7,0.9)')
+ roughness.outputs[0].default_value = uniform(0.7, 0.9)
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 0.1000, 'Detail': 9.0000, 'Roughness': roughness, 'Distortion': 0.2000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: noise_texture.outputs["Fac"], 1: 20.0000}, attrs={'operation': 'MULTIPLY'})
+
+ random_plane_angle = uniform(0, 2 * np.pi)
+
+ dot_product = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: mapping, 1: (np.cos(random_plane_angle), np.sin(random_plane_angle), 0.0000)},
+ attrs={'operation': 'DOT_PRODUCT'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: dot_product.outputs["Value"]})
+
+ sine = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'SINE'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: sine, 1: 1.0000})
+
+ darkness = nw.new_node(Nodes.Value, label='darkness ~ U(0,1)')
+ darkness.outputs[0].default_value = uniform(0.0, 1.0)
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': darkness, 3: 0.2000, 4: 0.3000})
+
+ power = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: map_range.outputs["Result"]}, attrs={'operation': 'POWER'})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': power})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_material_001, selection=selection)
\ No newline at end of file
diff --git a/infinigen/assets/materials/marble_voronoi.py b/infinigen/assets/materials/marble_voronoi.py
new file mode 100644
index 000000000..b1119c8b3
--- /dev/null
+++ b/infinigen/assets/materials/marble_voronoi.py
@@ -0,0 +1,53 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Zeyu Ma
+# Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=wTzk9T06gdw by Ryan King Art
+
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core import surface
+
+
+
+def shader_material(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ geometry = nw.new_node(Nodes.NewGeometry)
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': geometry.outputs["Position"]})
+
+ roughness = nw.new_node(Nodes.Value, label='roughness ~ U(0.5,0.7)')
+ roughness.outputs[0].default_value = uniform(0.5, 0.7)
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 2.0000, 'Detail': 9.0000, 'Roughness': roughness})
+
+ random_plane_angle = uniform(0, 2 * np.pi)
+
+ dot_product = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: mapping, 1: (np.cos(random_plane_angle), np.sin(random_plane_angle), 0.0000)},
+ attrs={'operation': 'DOT_PRODUCT'})
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: noise_texture.outputs["Color"], 1: dot_product.outputs["Value"]})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture, input_kwargs={'Vector': add.outputs["Vector"]})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': voronoi_texture.outputs["Distance"]})
+ colorramp.color_ramp.elements[0].position = uniform(0.4, 0.5)
+ colorramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ colorramp.color_ramp.elements[1].position = 0.9600
+ colorramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"], 'Metallic': 0.5000, 'Roughness': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_material, selection=selection)
\ No newline at end of file
diff --git a/infinigen/assets/materials/metal/__init__.py b/infinigen/assets/materials/metal/__init__.py
new file mode 100644
index 000000000..c427a1357
--- /dev/null
+++ b/infinigen/assets/materials/metal/__init__.py
@@ -0,0 +1,56 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections.abc import Iterable
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.util.color import hsv2rgba, rgb2hsv
+from infinigen.core.util.random import random_general as rg, log_uniform
+from . import (
+ brushed_metal, galvanized_metal, grained_and_polished_metal, hammered_metal,
+ metal_basic,
+)
+from .. import common
+from ..bark_random import hex_to_rgb
+
+
+def apply(obj, selection=None, metal_color=None, **kwargs):
+ color = sample_metal_color(metal_color)
+ shader = get_shader()
+ common.apply(obj, shader, selection, base_color=color, **kwargs)
+
+
+def get_shader():
+ return np.random.choice(
+ [brushed_metal.shader_brushed_metal, galvanized_metal.shader_galvanized_metal,
+ grained_and_polished_metal.shader_grained_metal,
+ hammered_metal.shader_hammered_metal]
+ )
+
+
+plain_colors = 'weighted_choice', (.5, 0xfdd017), (1, 0xc0c0c0), (1, 0x8c7853), (.5, 0xb87333), (.5, 0xb5a642), (
+ 1, 0xbdbaae), (1, 0xa9acb6), (1, 0xb6afa9)
+natural_colors = 'weighted_choice', (1, 0xc0c0c0), (1, 0x8c7853), (1, 0xbdbaae), (1, 0xa9acb6), (1, 0xb6afa9)
+
+
+def sample_metal_color(metal_color=None, **kwargs):
+ match metal_color:
+ case np.ndarray():
+ return metal_color
+ case 'plain':
+ h, s, v = rgb2hsv(hex_to_rgb(rg(plain_colors))[:-1])
+ return hsv2rgba(h + uniform(-.1, .1), s + uniform(-.1, .1), v * log_uniform(.5, .2))
+ case 'natural':
+ h, s, v = rgb2hsv(hex_to_rgb(rg(natural_colors))[:-1])
+ return hsv2rgba(h + uniform(-.1, .1), s + uniform(-.1, .1), v * log_uniform(.5, .2))
+ case 'bw':
+ return hsv2rgba(uniform(0, 1), uniform(.0, .2), log_uniform(.01, .2))
+ case 'bw+natural':
+ return sample_metal_color('bw') if uniform() < .5 else sample_metal_color('natural')
+ case _:
+ if uniform() < .2:
+ return sample_metal_color('natural')
+ return hsv2rgba(uniform(0, 1), uniform(.3, .6), log_uniform(.02, .5))
diff --git a/infinigen/assets/materials/metal/brushed_metal.py b/infinigen/assets/materials/metal/brushed_metal.py
new file mode 100644
index 000000000..18d879394
--- /dev/null
+++ b/infinigen/assets/materials/metal/brushed_metal.py
@@ -0,0 +1,87 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=QcAMYRgR03k by blenderian
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+@node_utils.to_nodegroup('nodegroup_brushed_metal', singleton=False, type='ShaderNodeTree')
+def nodegroup_brushed_metal(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (0.2000, 0.2000, 5.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 100.0000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_2 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Roughness': 0.4000, 'Distortion': 0.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (1.0000, 1.0000, 20.0000)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': 0.1000, 'Detail': 15.0000, 'Roughness': 0.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.2000, 6: mapping, 7: noise_texture_1.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mix.outputs[2], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Roughness': 0.6000, 'Distortion': 0.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: noise_texture_2.outputs["Fac"], 7: noise_texture.outputs["Fac"]},
+ attrs={'blend_type': 'DARKEN', 'data_type': 'RGBA'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': mix_1, 1: 0.4000, 2: 0.6000, 3: 0.8000, 4: 1.2000})
+
+ hue_saturation_value = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Value': map_range.outputs["Result"], 'Color': group_input.outputs["Base Color"]})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': mix_1, 1: 0.4000, 2: 0.6000, 3: 0.2000, 4: 0.3000})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': hue_saturation_value, 'Metallic': 1.0000, 'Specular': 0.0000, 'Roughness': map_range_1.outputs["Result"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'tmp_viewer': principled_bsdf},
+ attrs={'is_active_output': True})
+
+def shader_brushed_metal(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ from infinigen.assets.materials.metal import sample_metal_color
+ base_color = sample_metal_color(**kwargs)
+
+ group = nw.new_node(nodegroup_brushed_metal().name,
+ input_kwargs={'Base Color': base_color, 'Scale': scale, 'Seed': seed})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs['BSDF']},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_brushed_metal, selection=selection, input_kwargs=kwargs)
diff --git a/infinigen/assets/materials/metal/galvanized_metal.py b/infinigen/assets/materials/metal/galvanized_metal.py
new file mode 100644
index 000000000..ddf6a4040
--- /dev/null
+++ b/infinigen/assets/materials/metal/galvanized_metal.py
@@ -0,0 +1,68 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=ECl2pQ1jQm8 by Ryan King Art
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+@node_utils.to_nodegroup('nodegroup_galvanized_metal', singleton=False, type='ShaderNodeTree')
+def nodegroup_galvanized_metal(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000)])
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 5.0000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Roughness': 0.4000, 'Distortion': 0.2000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.0500, 6: texture_coordinate.outputs["Object"], 7: noise_texture.outputs["Color"]},
+ attrs={'clamp_factor': False, 'data_type': 'RGBA'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 500.0000}, attrs={'operation': 'MULTIPLY'})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mix.outputs[2], 'W': group_input.outputs["Seed"], 'Scale': multiply_1},
+ attrs={'distance': 'MINKOWSKI', 'voronoi_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': voronoi_texture.outputs["Color"], 3: 0.1000, 4: 0.5000})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': group_input.outputs["Base Color"], 'Metallic': 1.0000, 'Specular': 0.0000, 'Roughness': map_range.outputs["Result"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'BSDF': principled_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_galvanized_metal(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ from infinigen.assets.materials.metal import sample_metal_color
+ base_color = sample_metal_color(**kwargs)
+
+ group = nw.new_node(nodegroup_galvanized_metal().name,
+ input_kwargs={'Base Color': base_color, 'Scale': scale, 'Seed': seed})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': group}, attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_galvanized_metal, selection=selection,**kwargs)
diff --git a/infinigen/assets/materials/metal/grained_and_polished_metal.py b/infinigen/assets/materials/metal/grained_and_polished_metal.py
new file mode 100644
index 000000000..5e63f8219
--- /dev/null
+++ b/infinigen/assets/materials/metal/grained_and_polished_metal.py
@@ -0,0 +1,79 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+@node_utils.to_nodegroup('nodegroup_grained_metal', singleton=False, type='ShaderNodeTree')
+def nodegroup_grained_metal(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 5.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloat', 'Roughness', 0.0000)])
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': group_input.outputs["Roughness"], 3: 0.0500, 4: 0.2500})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': group_input.outputs["Base Color"], 'Metallic': 1.0000, 'Specular': 0.0000, 'Roughness': map_range.outputs["Result"]})
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Scale"], 1: 2000.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Distortion': 2.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: map_range.outputs["Result"], 1: 0.4000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: noise_texture.outputs["Fac"], 1: multiply_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: 0.010}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': multiply_3},
+ attrs={'is_active_output': True})
+
+
+def shader_grained_metal(nw: NodeWrangler, scale=1.0, base_color=None, roughness=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if roughness is None:
+ roughness = uniform(0.0, 1.0)
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ from infinigen.assets.materials.metal import sample_metal_color
+ base_color = sample_metal_color(**kwargs)
+
+
+ group = nw.new_node(nodegroup_grained_metal().name,
+ input_kwargs={'Base Color': base_color,
+ 'Scale': scale,
+ 'Seed': seed,
+ 'Roughness': roughness,
+ })
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_grained_metal, selection=selection, input_kwargs=kwargs)
diff --git a/infinigen/assets/materials/metal/hammered_metal.py b/infinigen/assets/materials/metal/hammered_metal.py
new file mode 100644
index 000000000..d87a27f2b
--- /dev/null
+++ b/infinigen/assets/materials/metal/hammered_metal.py
@@ -0,0 +1,81 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=82smQvoh0GE by Mix CG Arts
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.util.random import log_uniform
+
+
+@node_utils.to_nodegroup('nodegroup_hammered_metal', singleton=False, type='ShaderNodeTree')
+def nodegroup_hammered_metal(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000)])
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': group_input.outputs["Base Color"], 'Metallic': 1.0000, 'Specular': 0.0000, 'Roughness': 0.1000})
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 20.0000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Roughness': 0.4000, 'Distortion': 0.2000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.0100, 6: texture_coordinate.outputs["Object"], 7: noise_texture.outputs["Color"]},
+ attrs={'clamp_factor': False, 'data_type': 'RGBA'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 300.0000}, attrs={'operation': 'MULTIPLY'})
+
+ voronoi_texture_1 = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mix.outputs[2], 'W': group_input.outputs["Seed"], 'Scale': multiply_1, 'Smoothness': 0.2000},
+ attrs={'voronoi_dimensions': '4D', 'feature': 'SMOOTH_F1'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: voronoi_texture_1.outputs["Distance"], 1: group_input.outputs["Scale"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ power = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: 2.5000}, attrs={'operation': 'POWER'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: power, 1: log_uniform(.001,0.003)}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': multiply_3, 'tmp_viewer': voronoi_texture_1.outputs["Color"]},
+ attrs={'is_active_output': True})
+
+def shader_hammered_metal(nw: NodeWrangler, scale=None, base_color=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ from infinigen.assets.materials.metal import sample_metal_color
+ base_color = sample_metal_color(**kwargs)
+ if scale is None:
+ scale = log_uniform(.8, 1.2)
+
+ group = nw.new_node(nodegroup_hammered_metal().name,
+ input_kwargs={'Base Color': base_color, 'Scale': scale, 'Seed': seed})
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_material(obj, shader_hammered_metal, selection=selection, input_kwargs=kwargs)
diff --git a/infinigen/assets/materials/metal/metal_basic.py b/infinigen/assets/materials/metal/metal_basic.py
new file mode 100644
index 000000000..81d5b96b6
--- /dev/null
+++ b/infinigen/assets/materials/metal/metal_basic.py
@@ -0,0 +1,33 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+
+
+def shader_metal(nw: NodeWrangler, color=None, **kwargs):
+ position = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ roughness = nw.build_float_curve(
+ nw.new_node(Nodes.NoiseTexture, [position], input_kwargs={'Scale': uniform(10, 25)}),
+ [(0, uniform(0, .2)), (1, uniform(.4, .7))]
+ )
+ principled_bsdf = nw.new_node(
+ Nodes.PrincipledBSDF, input_kwargs={
+ "Metallic": 1.,
+ 'Specular': uniform(.5, 1.),
+ 'Base Color': color,
+ 'Roughness': roughness
+ }
+ )
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf})
+
+
+def apply(obj, selection=None, **kwargs):
+ from infinigen.assets.materials.metal import sample_metal_color
+ color = sample_metal_color(**kwargs)
+ common.apply(obj, shader_metal, selection, color, **kwargs)
diff --git a/infinigen/assets/materials/microwave_shaders.py b/infinigen/assets/materials/microwave_shaders.py
new file mode 100644
index 000000000..f08b4ff42
--- /dev/null
+++ b/infinigen/assets/materials/microwave_shaders.py
@@ -0,0 +1,27 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+def shader_black_medal(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ anisotropic_bsdf = nw.new_node('ShaderNodeBsdfAnisotropic', input_kwargs={'Color': (0.0167, 0.0167, 0.0167, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': anisotropic_bsdf}, attrs={'is_active_output': True})
+
+def shader_black_glass(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ glossy_bsdf = nw.new_node(Nodes.GlossyBSDF, input_kwargs={'Color': (0.0068, 0.0068, 0.0068, 1.0000), 'Roughness': 0.2000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glossy_bsdf}, attrs={'is_active_output': True})
+
+def shader_glass(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ glass_bsdf = nw.new_node(Nodes.GlassBSDF, input_kwargs={'IOR': 1.5000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/materials/mirror.py b/infinigen/assets/materials/mirror.py
new file mode 100644
index 000000000..a725fce4e
--- /dev/null
+++ b/infinigen/assets/materials/mirror.py
@@ -0,0 +1,18 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from infinigen.assets.materials import common
+from infinigen.core.nodes import NodeWrangler, Nodes
+
+
+def shader_mirror(nw: NodeWrangler,**kwargs):
+ glossy_bsdf = nw.new_node('ShaderNodeBsdfGlossy',
+ input_kwargs={'Color': (1.0, 1.0, 1.0, 1.0), 'Roughness': 0,
+ })
+
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glossy_bsdf})
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_mirror, selection, **kwargs)
diff --git a/infinigen/assets/materials/oven_shaders.py b/infinigen/assets/materials/oven_shaders.py
new file mode 100644
index 000000000..c6e3017e9
--- /dev/null
+++ b/infinigen/assets/materials/oven_shaders.py
@@ -0,0 +1,28 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+
+def shader_super_black_glass(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+ glossy_bsdf = nw.new_node(Nodes.GlossyBSDF, input_kwargs={'Color': (0.0095, 0.0095, 0.0095, 1.0000), 'Roughness': 0.0000})
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glossy_bsdf}, attrs={'is_active_output': True})
+
+
+def shader_black_medal(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ anisotropic_bsdf = nw.new_node('ShaderNodeBsdfAnisotropic', input_kwargs={'Color': (0.0167, 0.0167, 0.0167, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': anisotropic_bsdf}, attrs={'is_active_output': True})
+
+def shader_glass(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ glass_bsdf = nw.new_node(Nodes.GlassBSDF, input_kwargs={'IOR': 1.5000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True})
+
diff --git a/infinigen/assets/materials/plaster.py b/infinigen/assets/materials/plaster.py
new file mode 100644
index 000000000..918108828
--- /dev/null
+++ b/infinigen/assets/materials/plaster.py
@@ -0,0 +1,59 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections.abc import Iterable
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.object import new_plane
+from infinigen.assets.utils.uv import unwrap_normal
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+from infinigen.assets.materials import common
+from infinigen.core.util.random import log_uniform
+from infinigen.core.nodes.node_utils import build_color_ramp
+
+
+def shader_plaster(nw: NodeWrangler, plaster_colored, **kwargs):
+ hue = uniform(0, 1)
+ front_value = log_uniform(.5, 1.)
+ back_value = front_value * uniform(.6, 1)
+ if plaster_colored:
+ front_color = hsv2rgba(hue, uniform(.3, .5), front_value)
+ back_color = hsv2rgba(hue + uniform(-.1, .1), uniform(.3, .5), back_value)
+ else:
+ front_color = hsv2rgba(hue, 0, front_value)
+ back_color = hsv2rgba(hue + uniform(-.1, .1), 0, back_value)
+ uv_map = nw.new_node(Nodes.UVMap)
+ musgrave = nw.new_node(Nodes.MusgraveTexture, [uv_map],
+ input_kwargs={'Detail': log_uniform(15, 30), 'Dimension': 0})
+ noise = nw.new_node(Nodes.NoiseTexture, [uv_map],
+ input_kwargs={'Detail': log_uniform(15, 30), 'Distortion': log_uniform(4, 8)})
+ noise = build_color_ramp(nw, noise, [0, uniform(.3, .5)], [(0, 0, 0, 1), (1, 1, 1, 1)])
+ difference = nw.new_node(Nodes.MixRGB, [musgrave, noise], attrs={'blend_type': 'DIFFERENCE'})
+ base_color = build_color_ramp(nw, difference, [uniform(.2, .3), 1], [back_color, front_color])
+
+ displacement = nw.new_node(Nodes.Displacement, input_kwargs={
+ 'Scale': log_uniform(.0001, .0003),
+ 'Height': nw.new_node(Nodes.MusgraveTexture, input_kwargs={'Scale': uniform(1e3, 2e3)})
+ })
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': base_color,
+ 'Roughness': uniform(.7, .8),
+ })
+
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement})
+
+
+def apply(obj, selection=None, plaster_colored=None, **kwargs):
+ if plaster_colored is None:
+ plaster_colored = uniform() < .4
+ for o in obj if isinstance(obj, Iterable) else [obj]:
+ unwrap_normal(o, selection)
+ common.apply(obj, shader_plaster, selection, plaster_colored=plaster_colored, **kwargs)
+
+
diff --git a/infinigen/assets/materials/plastic.py b/infinigen/assets/materials/plastic.py
new file mode 100644
index 000000000..1dd6912b8
--- /dev/null
+++ b/infinigen/assets/materials/plastic.py
@@ -0,0 +1,24 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the GPL license found in the LICENSE file in the root directory of this
+# source tree.
+
+# Authors: Mingzhe Wang, Lingjie Mei
+
+import colorsys
+
+from infinigen.assets.materials.plastics.plastic_rough import shader_rough_plastic
+from infinigen.assets.materials.plastics.plastic_translucent import shader_translucent_plastic
+from infinigen.core.util.color import hsv2rgba
+
+from infinigen.assets.materials import common
+from numpy.random import uniform
+
+
+
+def apply(obj, selection=None, clear=None, **kwargs):
+ is_rough = kwargs.get('rough', uniform(0, 1))
+ is_translucent = kwargs.get('translucent', uniform(0, 1))
+ if clear is None:
+ clear = uniform() < .2
+ shader_func = shader_rough_plastic if is_rough > is_translucent else shader_translucent_plastic
+ common.apply(obj, shader_func, selection, clear=clear, **kwargs)
diff --git a/infinigen/assets/materials/plastics/plastic_rough.py b/infinigen/assets/materials/plastics/plastic_rough.py
new file mode 100644
index 000000000..c2db3bd0e
--- /dev/null
+++ b/infinigen/assets/materials/plastics/plastic_rough.py
@@ -0,0 +1,83 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the GPL license found in the LICENSE file in the root directory of this
+# source tree.
+
+# Authors: Mingzhe Wang, Lingjie Mei
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+
+@node_utils.to_nodegroup('nodegroup_plastics', singleton=False, type='ShaderNodeTree')
+def nodegroup_plastics(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 5.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloat', 'Roughness', 0.0000)])
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': group_input.outputs["Roughness"], 3: 0.0500, 4: 0.2500})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': group_input.outputs["Base Color"], 'Roughness': map_range.outputs["Result"]})
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Scale"], 1: 2000.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000, 'Distortion': 2.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: map_range.outputs["Result"], 1: 0.4000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: noise_texture.outputs["Fac"], 1: multiply_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: 0.0030}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': multiply_3},
+ attrs={'is_active_output': True})
+
+def shader_rough_plastic(nw: NodeWrangler, scale=1.0, base_color=None, roughness=None, seed=None, clear=False, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if roughness is None:
+ roughness = uniform(0.0, 1.0)
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ if clear:
+ base_color = hsv2rgba(0, 0, log_uniform(.02, .8))
+ else:
+ base_color = hsv2rgba(uniform(0, 1), uniform(.5, .8), log_uniform(.01, .5))
+
+ group = nw.new_node(nodegroup_plastics().name,
+ input_kwargs={'Base Color': base_color,
+ 'Scale': scale,
+ 'Seed': seed,
+ 'Roughness': roughness,
+ })
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_rough_plastic, selection, **kwargs)
\ No newline at end of file
diff --git a/infinigen/assets/materials/plastics/plastic_translucent.py b/infinigen/assets/materials/plastics/plastic_translucent.py
new file mode 100644
index 000000000..d41a04264
--- /dev/null
+++ b/infinigen/assets/materials/plastics/plastic_translucent.py
@@ -0,0 +1,44 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the GPL license found in the LICENSE file in the root directory of this
+# source tree.
+
+# Authors: Mingzhe Wang, Lingjie Mei
+
+import colorsys
+
+from infinigen.core.util.color import hsv2rgba
+from infinigen.assets.materials import common
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.materials.utils.surface_utils import sample_range
+from numpy.random import uniform
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+
+def shader_translucent_plastic(nw: NodeWrangler, clear=False, **input_kwargs):
+ # Code generated using version 2.4.3 of the node_transpiler
+
+ layer_weight = nw.new_node('ShaderNodeLayerWeight', input_kwargs={'Blend': sample_range(0.2, 0.4)})
+
+ rgb = nw.new_node(Nodes.RGB)
+
+ if clear:
+ base_color = hsv2rgba(0, 0, log_uniform(.4, .8))
+ else:
+ base_color = hsv2rgba(uniform(0, 1), uniform(.5, .8), log_uniform(.4, .8))
+ rgb.outputs[0].default_value = base_color
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = sample_range(1.2, 1.6)
+
+ glass_bsdf = nw.new_node('ShaderNodeBsdfGlass', input_kwargs={'Color': rgb, 'Roughness': 0.2, 'IOR': value})
+
+ glossy_bsdf = nw.new_node('ShaderNodeBsdfGlossy', input_kwargs={'Roughness': 0.2})
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': layer_weight.outputs["Fresnel"], 1: glass_bsdf, 2: glossy_bsdf
+ })
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': mix_shader})
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_translucent_plastic, selection, **kwargs)
\ No newline at end of file
diff --git a/infinigen/assets/materials/rug.py b/infinigen/assets/materials/rug.py
new file mode 100644
index 000000000..4f03d19f2
--- /dev/null
+++ b/infinigen/assets/materials/rug.py
@@ -0,0 +1,50 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
+
+
+def shader_rug(nw: NodeWrangler, strength=1., **kwargs):
+ coord = nw.new_node(Nodes.Mapping, [nw.new_node(Nodes.TextureCoord).outputs['Object']])
+ vec = nw.new_node(Nodes.MixRGB, [uniform(.8, .9), nw.new_node(Nodes.NoiseTexture, [coord]), coord])
+ height = 0, 0, 0, 1
+ base_scale = log_uniform(250, 500)
+ for scale, thresh in zip([1, .75, .5], [1, .5, .33]):
+ voronoi = nw.new_node(Nodes.VoronoiTexture, [vec], input_kwargs={'Scale': scale * base_scale}).outputs[
+ 0]
+ height = nw.new_node(Nodes.MixRGB, [nw.math('GREATER_THAN', voronoi, thresh), voronoi, height])
+
+ displacement = nw.new_node(Nodes.Displacement, input_kwargs={
+ 'Scale': strength,
+ 'Height': height
+ })
+
+ base_hue = uniform(0, 1)
+ base_value = uniform(.2, .5)
+ if uniform() < .2:
+ base_saturation = log_uniform(.02, .05)
+ front_color = hsv2rgba(base_hue, base_saturation, base_value)
+ back_color = hsv2rgba(base_hue + uniform(-.01, .01), base_saturation * uniform(.9, 1.1),
+ base_value * uniform(.9, 1.1))
+ else:
+ base_saturation = log_uniform(.2, .4)
+ front_color = hsv2rgba(base_hue, base_saturation, base_value)
+ back_color = hsv2rgba(base_hue + uniform(-.01, .01), base_saturation * uniform(.9, 1.1),
+ base_value * uniform(.9, 1.1))
+ color = nw.new_node(Nodes.MixRGB, [
+ nw.build_float_curve(nw.musgrave(uniform(20, 50)), [(0, 1), (uniform(.3, .4), 0), (1, 0)]), front_color,
+ back_color])
+ roughness = nw.build_float_curve(nw.musgrave(uniform(20, 50)), [(.5, .9), (1, .8)])
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': color, 'Roughness': roughness})
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement})
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_rug, selection, **kwargs)
diff --git a/infinigen/assets/materials/sandstone.py b/infinigen/assets/materials/sandstone.py
index 448d7afa6..c9b63bdea 100644
--- a/infinigen/assets/materials/sandstone.py
+++ b/infinigen/assets/materials/sandstone.py
@@ -4,7 +4,7 @@
# Authors: Ankit Goyal, Mingzhe Wang, Zeyu Ma
-
+
# Code generated using version v2.0.0 of the node_transpiler
import gin
from infinigen.core.nodes import node_utils
@@ -410,7 +410,7 @@ def geometry_sandstone(nw, selection=None, is_rock=False, **kwargs):
)
normal = nw.new_node("GeometryNodeInputNormal", [])
-
+
group_3 = nw.new_node(
nodegroup_roughness().name,
input_kwargs={"Noise 1 Scale": 200.0, "Noise 1 Magnitude": 0.5, 'Normal': normal},
@@ -556,7 +556,7 @@ def geometry_sandstone(nw, selection=None, is_rock=False, **kwargs):
nw.new_value(0.2, "stripe_warp_mag"),
)
)
-
+
offset2 = nw.add(
multiply_3,
nw.multiply(
@@ -571,7 +571,7 @@ def geometry_sandstone(nw, selection=None, is_rock=False, **kwargs):
normal,
)
)
-
+
noise_params = {"scale": ("uniform", 10, 20), "detail": 9, "roughness": 0.6, "zscale": ("log_uniform", 0.05, 0.1)}
offset = nw.add(
diff --git a/infinigen/assets/materials/shelf_shaders.py b/infinigen/assets/materials/shelf_shaders.py
new file mode 100644
index 000000000..ed92d64ad
--- /dev/null
+++ b/infinigen/assets/materials/shelf_shaders.py
@@ -0,0 +1,303 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+# Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=jDEijCwz6to by Lachlan Sarv
+
+import numpy as np
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials import (
+ metal_shader_list,
+ shader_glass,
+ shader_rough_plastic,
+ wood,
+)
+from infinigen.assets.materials.leather_and_fabrics import fabric_shader_list
+from infinigen.core import surface
+
+
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+
+import json
+from infinigen.core.util.math import FixedSeed, int_hash
+from infinigen.core.util.random import random_general as rg
+
+
+def shader_shelves_white(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ rgb = kwargs.get('rgb', [0.9, 0.9, 0.9])
+ base_color = (*rgb, 1.)
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': base_color,
+ 'Roughness': kwargs.get('roughness', 0.9)})
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_shelves_white_sampler():
+ params = dict()
+ v = uniform(0.7, 1.0)
+ base_color = [v * (1. + normal(0, 0.005)),
+ v * (1. + normal(0, 0.005)),
+ v * (1. + normal(0, 0.005))]
+ params['rgb'] = base_color
+ params['roughness'] = uniform(0.7, 1.0)
+ return params
+
+
+def shader_shelves_black_metallic(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ color = (*kwargs.get('rgb', [0., 0., 0.]), 1.)
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={
+ 'Base Color': color,
+ 'Metallic': kwargs.get('metallic', 0.65)})
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_shelves_black_metallic_sampler():
+ params = dict()
+ base_color = [uniform(0, 0.01), uniform(0, 0.01), uniform(0, 0.01)]
+ params['rgb'] = base_color
+ params['metallic'] = uniform(0.45, 0.75)
+ return params
+
+
+def shader_shelves_white_metallic(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ rgb = kwargs.get('rgb', [0.9, 0.9, 0.9])
+ base_color = (*rgb, 1.)
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': base_color,
+ 'Metallic': kwargs.get('metallic', 0.65)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_shelves_white_metallic_sampler():
+ params = dict()
+ v = uniform(0.7, 1.0)
+ base_color = [v * (1. + normal(0, 0.005)),
+ v * (1. + normal(0, 0.005)),
+ v * (1. + normal(0, 0.005))]
+ params['rgb'] = base_color
+ params['metallic'] = uniform(0.45, 0.75)
+ return params
+
+
+def shader_shelves_black_wood(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
+ wave_scale = kwargs.get('wave_scale', 2.0)
+ if kwargs.get('z_axis_texture', False):
+ wave_scale = (wave_scale, wave_scale, 0.1)
+ else:
+ wave_scale = (wave_scale, 0.1, 0.1)
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"],
+ 'Scale': (0.1, 0.1, 2.0) if kwargs.get('z_axis_texture', False) else (0.1, 2.0, 2.0)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'Scale': 100.0000, 'Detail': 10.0000,
+ 'Distortion': 2.0000})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture_1.outputs["Fac"], 'Scale': 40.0000})
+
+ colorramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': voronoi_texture.outputs["Color"]})
+ colorramp_1.color_ramp.elements[0].position = 0.0864
+ colorramp_1.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ colorramp_1.color_ramp.elements[1].position = 0.1091
+ colorramp_1.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"],
+ 'Scale': wave_scale})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 3.0000, 'Detail': 15.0000,
+ 'Distortion': 2.0000})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'Scale': 20.0000,
+ 'Detail': 3.0000})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={6: musgrave_texture, 7: noise_texture.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ colorramp_2 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_1.outputs[2]})
+ colorramp_2.color_ramp.elements[0].position = 0.0818
+ colorramp_2.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ colorramp_2.color_ramp.elements[1].position = 0.8500
+ colorramp_2.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.6000, 6: colorramp_1.outputs["Color"], 7: colorramp_2.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ dark_scale = kwargs.get('dark_scale', 0.005)
+ gray_scale = kwargs.get('gray_scale', 0.02)
+ color_scale = [*kwargs.get('rgb', [0.02, 0.002, 0.002]), 1.0]
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_2})
+ colorramp.color_ramp.elements.new(0)
+ colorramp.color_ramp.elements[0].position = 0.15
+ colorramp.color_ramp.elements[0].color = [dark_scale, dark_scale, dark_scale, 1.0000]
+ colorramp.color_ramp.elements[1].position = 0.5
+ colorramp.color_ramp.elements[1].color = [gray_scale, gray_scale, gray_scale, 1.0000]
+ colorramp.color_ramp.elements[2].position = 1.0000
+ colorramp.color_ramp.elements[2].color = color_scale
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.0040, 6: colorramp_1.outputs["Color"], 7: colorramp_2.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node(Nodes.Bump, input_kwargs={'Strength': 0.5000, 'Height': mix_3.outputs[2]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"],
+ 'Roughness': kwargs.get('roughness', 0.9), 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_shelves_black_wood_sampler():
+ params = dict()
+ params['wave_scale'] = uniform(1., 3.)
+ params['dark_scale'] = uniform(0.0, 0.01)
+ params['gray_scale'] = uniform(0.01, 0.03)
+ params['rgb'] = [uniform(0.015, 0.035), uniform(0., 0.01), uniform(0.0, 0.01)]
+ params['roughness'] = uniform(0.75, 1.0)
+ return params
+
+
+def shader_shelves_wood(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
+ wave_scale = kwargs.get('wave_scale', 2.0)
+ if kwargs.get('z_axis_texture', False):
+ wave_scale = (wave_scale, wave_scale, 0.1)
+ else:
+ wave_scale = (wave_scale, 0.1, 0.1)
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"],
+ 'Scale': (0.1, 0.1, 2.0) if kwargs.get('z_axis_texture', False) else (0.1, 2.0, 2.0)})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'Scale': 100.0000, 'Detail': 10.0000,
+ 'Distortion': 2.0000})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture_1.outputs["Fac"], 'Scale': 40.0000})
+
+ colorramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': voronoi_texture.outputs["Color"]})
+ colorramp_1.color_ramp.elements[0].position = 0.0864
+ colorramp_1.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ colorramp_1.color_ramp.elements[1].position = 0.1091
+ colorramp_1.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"],
+ 'Scale': wave_scale})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': 3.0000, 'Detail': 15.0000,
+ 'Distortion': 2.0000})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'Scale': 20.0000,
+ 'Detail': 3.0000})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={6: musgrave_texture, 7: noise_texture.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ colorramp_2 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_1.outputs[2]})
+ colorramp_2.color_ramp.elements[0].position = 0.0818
+ colorramp_2.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ colorramp_2.color_ramp.elements[1].position = 0.8500
+ colorramp_2.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.6000, 6: colorramp_1.outputs["Color"], 7: colorramp_2.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ bright_hsv = kwargs.get('bright_hsv', [0.068, 0.665, 0.805])
+ mid_hsv = kwargs.get('mid_hsv', [0.042, 0.853, 0.447])
+ dark_hsv = kwargs.get('dark_hsv', [0.043, 0.882, 0.183])
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_2})
+ colorramp.color_ramp.elements.new(0)
+ colorramp.color_ramp.elements[0].position = 0.02
+ colorramp.color_ramp.elements[0].color = hsv2rgba(dark_hsv)
+ colorramp.color_ramp.elements[1].position = 0.11
+ colorramp.color_ramp.elements[1].color = hsv2rgba(mid_hsv)
+ colorramp.color_ramp.elements[2].position = 0.8
+ colorramp.color_ramp.elements[2].color = hsv2rgba(bright_hsv)
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.0040, 6: colorramp_1.outputs["Color"], 7: colorramp_2.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node(Nodes.Bump, input_kwargs={'Strength': 0.5000, 'Height': mix_3.outputs[2]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': colorramp.outputs["Color"],
+ 'Roughness': kwargs.get('roughness', 0.9), 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_shelves_wood_sampler():
+ params = dict()
+ params['bright_hsv'] = [uniform(0.03, 0.09), uniform(0.5, 0.7), uniform(0.7, 1.0)]
+ params['mid_hsv'] = [uniform(0.02, 0.06), uniform(0.6, 1.0), uniform(0.3, 0.6)]
+ params['dark_hsv'] = [uniform(0.03, 0.05), uniform(0.6, 1.0), uniform(0.1, 0.3)]
+ params['wave_scale'] = uniform(1., 3.)
+ params['roughness'] = uniform(0.75, 1.0)
+ return params
+
+
+def get_shelf_material(name, **kwargs):
+ match name:
+ case 'white':
+ shader_func = np.random.choice([shader_shelves_white, shader_rough_plastic], p=[.6, .4])
+ case 'black_wood':
+ shader_func = np.random.choice([shader_shelves_black_wood, wood.shader_wood], p=[.6, .4])
+ case 'wood':
+ shader_func = np.random.choice([shader_shelves_wood, wood.shader_wood], p=[.6, .4])
+
+ case 'glass':
+ shader_func = shader_glass
+ case _:
+ shader_func = np.random.choice([shader_shelves_white, shader_rough_plastic,
+ shader_shelves_black_wood, wood.shader_wood,
+ shader_shelves_wood], p=[.3, .2, .3, .1, .1])
+ r = uniform()
+ if name == 'metal':
+ shader_func = np.random.choice(metal_shader_list)
+ else:
+ shader_func = np.random.choice([shader_shelves_white, shader_rough_plastic,
+ shader_shelves_black_wood, wood.shader_wood,
+ shader_shelves_wood], p=[.3, .2, .3, .1, .1])
+ # elif r < .3:
+ # shader_func = rg(fabric_shader_list)
+ return surface.shaderfunc_to_material(shader_func, **kwargs)
diff --git a/infinigen/assets/materials/soil.py b/infinigen/assets/materials/soil.py
index 48c7e7077..02029a439 100644
--- a/infinigen/assets/materials/soil.py
+++ b/infinigen/assets/materials/soil.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Ankit Goyal, Zeyu Ma
+# Authors: Ankit Goyal, Zeyu Ma, Lingjie Mei
import gin
@@ -58,7 +58,7 @@ def nodegroup_pebble(nw):
position = nw.new_node('ShaderNodeNewGeometry')
else:
position = nw.new_node(Nodes.InputPosition)
-
+
# Code generated using version 2.3.1 of the node_transpiler
noise1_w = nw.new_node(Nodes.Value, label="noise1_w ~ U(0, 10)")
@@ -122,9 +122,9 @@ def nodegroup_pebble_shader(nw):
nodegroup_pebble(nw)
-def shader_soil(nw):
+def shader_soil(nw, random_seed=0):
nw.force_input_consistency()
- big_stone = geometry_soil(nw, geometry=False)
+ big_stone = geometry_soil(nw, random_seed=random_seed, geometry=False)
# Code generated using version 2.3.1 of the node_transpiler
darkness = 1.5
soil_col_1 = random_color_neighbour((0.28 / darkness, 0.11 / darkness, 0.042 / darkness, 1.0), 0.05, 0.1, 0.1)
@@ -220,7 +220,7 @@ def geometry_soil(nw, selection=None, random_seed=0, geometry=True):
else:
position = nw.new_node(Nodes.InputPosition)
normal = nw.new_node(Nodes.InputNormal)
-
+
with FixedSeed(random_seed):
# Code generated using version 2.3.1 of the node_transpiler
diff --git a/infinigen/assets/materials/stone.py b/infinigen/assets/materials/stone.py
index f05b5a392..185fb12c8 100644
--- a/infinigen/assets/materials/stone.py
+++ b/infinigen/assets/materials/stone.py
@@ -23,9 +23,9 @@
mod_name = "geo_stone"
name = "stone"
-def shader_stone(nw):
+def shader_stone(nw, random_seed=0):
nw.force_input_consistency()
- stone_base_color, stone_roughness = geo_stone(nw, geometry=False)
+ stone_base_color, stone_roughness = geo_stone(nw, random_seed=random_seed, geometry=False)
principled_bsdf = nw.new_node(
Nodes.PrincipledBSDF,
@@ -193,26 +193,26 @@ def geo_stone(nw, selection=None, random_seed=0, geometry=True):
noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': position, "W": nw.new_value(uniform(0, 10), "noise_texture_3_w"), 'Scale': nw.new_value(sample_ratio(5, 3/4, 4/3), "noise_texture_3_scale")},
attrs={"noise_dimensions": "4D"})
-
+
subtract = nw.new_node(Nodes.Math,
input_kwargs={0: noise_texture_3.outputs["Fac"]},
attrs={'operation': 'SUBTRACT'})
-
+
multiply_8 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: subtract, 1: normal},
attrs={'operation': 'MULTIPLY'})
-
+
value_5 = nw.new_node(Nodes.Value)
value_5.outputs[0].default_value = 0.05
-
+
multiply_9 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: multiply_8.outputs["Vector"], 1: value_5},
attrs={'operation': 'MULTIPLY'})
-
+
noise_texture_4 = nw.new_node(Nodes.NoiseTexture,
input_kwargs={'Vector': position, 'Scale': nw.new_value(sample_ratio(20, 3/4, 4/3), "noise_texture_4_scale"), "W": nw.new_value(uniform(0, 10), "noise_texture_4_w")},
attrs={'noise_dimensions': '4D'})
-
+
colorramp_5 = nw.new_node(Nodes.ColorRamp,
input_kwargs={'Fac': noise_texture_4.outputs["Fac"]})
colorramp_5.color_ramp.elements.new(0)
@@ -225,18 +225,18 @@ def geo_stone(nw, selection=None, random_seed=0, geometry=True):
colorramp_5.color_ramp.elements[2].color = (0.5, 0.5, 0.5, 1.0)
colorramp_5.color_ramp.elements[3].position = 1.0
colorramp_5.color_ramp.elements[3].color = (1.0, 1.0, 1.0, 1.0)
-
+
subtract_1 = nw.new_node(Nodes.Math,
input_kwargs={0: colorramp_5.outputs["Color"]},
attrs={'operation': 'SUBTRACT'})
-
+
multiply_10 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: subtract_1, 1: normal},
attrs={'operation': 'MULTIPLY'})
-
+
value_6 = nw.new_node(Nodes.Value)
value_6.outputs[0].default_value = 0.1
-
+
multiply_11 = nw.new_node(Nodes.VectorMath,
input_kwargs={0: multiply_10.outputs["Vector"], 1: value_6},
attrs={'operation': 'MULTIPLY'})
@@ -258,7 +258,7 @@ def geo_stone(nw, selection=None, random_seed=0, geometry=True):
colorramp.color_ramp.elements[2].color = (color3, color3, color3, 1.0)
sample_color(colorramp.color_ramp.elements[1].color, offset=0.01)
sample_color(colorramp.color_ramp.elements[2].color, offset=0.01)
-
+
stone_base_color = nw.new_node(
Nodes.MixRGB,
input_kwargs={
@@ -280,7 +280,7 @@ def geo_stone(nw, selection=None, random_seed=0, geometry=True):
stone_roughness = colorramp_3
- if geometry:
+ if geometry:
groupinput = nw.new_node(Nodes.GroupInput)
noise_params = {"scale": ("uniform", 10, 20), "detail": 9, "roughness": 0.6, "zscale": ("log_uniform", 0.007, 0.013)}
offset = nw.add(offset, geo_MOUNTAIN_general(nw, 3, noise_params, 0, {}, {}))
diff --git a/infinigen/assets/materials/stone_and_concrete/concrete.py b/infinigen/assets/materials/stone_and_concrete/concrete.py
new file mode 100644
index 000000000..a41f00ec6
--- /dev/null
+++ b/infinigen/assets/materials/stone_and_concrete/concrete.py
@@ -0,0 +1,242 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=XDqRa0ExDqs by Ryan King Art
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+@node_utils.to_nodegroup('nodegroup_crack', singleton=False, type='ShaderNodeTree')
+def nodegroup_crack(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloat', 'Amount', 1.0000),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketFloatFactor', 'Snake Crack', 0.3000)])
+
+ texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
+
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': group_input.outputs["Scale"], 'Detail': 15.0000, 'Dimension': 0.2000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"]}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_2 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture_2.outputs["Fac"], 'Scale': 1.2000},
+ attrs={'feature': 'DISTANCE_TO_EDGE'})
+
+ map_range_4 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': voronoi_texture.outputs["Distance"], 2: 0.0200, 3: 2.0000, 4: 0.0000})
+
+ mix_7 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: group_input.outputs["Snake Crack"], 6: musgrave_texture_2, 7: map_range_4.outputs["Result"]},
+ attrs={'blend_type': 'ADD', 'data_type': 'RGBA'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 0.6000}, attrs={'operation': 'MULTIPLY'})
+
+ musgrave_texture_3 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply_1, 'Detail': 15.0000, 'Dimension': 1.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': group_input.outputs["Amount"], 3: 1.0000, 4: -0.5000})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: map_range_2.outputs["Result"], 1: 0.1000})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_3, 1: map_range_2.outputs["Result"], 2: add})
+
+ mix_4 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: mix_7.outputs[2], 7: map_range_1.outputs["Result"]},
+ attrs={'blend_type': 'DARKEN', 'data_type': 'RGBA'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Scale"], 1: 0.3000}, attrs={'operation': 'MULTIPLY'})
+
+ musgrave_texture_4 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'W': group_input.outputs["Seed"], 'Scale': multiply_2, 'Detail': 15.0000, 'Dimension': 1.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: map_range_2.outputs["Result"], 1: 0.1000})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_4, 1: map_range_2.outputs["Result"], 2: add_1})
+
+ mix_5 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: mix_4.outputs[2], 7: map_range.outputs["Result"]},
+ attrs={'blend_type': 'DARKEN', 'data_type': 'RGBA'})
+
+ color_ramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_5.outputs[2]})
+ color_ramp.color_ramp.elements[0].position = 0.0000
+ color_ramp.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 1.0000
+ color_ramp.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Color': color_ramp.outputs["Color"]}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_concrete', singleton=False, type='ShaderNodeTree')
+def nodegroup_concrete(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input_1 = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketColor', 'Base Color', (0.8000, 0.8000, 0.8000, 1.0000)),
+ ('NodeSocketFloat', 'Scale', 0.0000),
+ ('NodeSocketFloat', 'Seed', 0.0000),
+ ('NodeSocketFloat', 'Roughness', 0.0000),
+ ('NodeSocketFloat', 'Crack Amount', 0.0000),
+ ('NodeSocketFloat', 'Crack Scale', 0.0000),
+ ('NodeSocketFloatFactor', 'Snake Crack', 0.3000)])
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["Scale"], 1: 10.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input_1.outputs["Crack Scale"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ group = nw.new_node(nodegroup_crack().name,
+ input_kwargs={'Seed': group_input_1.outputs["Seed"], 'Amount': group_input_1.outputs["Crack Amount"], 'Scale': multiply_1, 'Snake Crack': group_input_1.outputs["Snake Crack"]})
+
+ map_range_3 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': group})
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Scale"], 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input_1.outputs["Seed"], 'Scale': multiply_2, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Scale"], 1: 5.0000}, attrs={'operation': 'MULTIPLY'})
+
+ musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input_1.outputs["Seed"], 'Scale': multiply_3, 'Detail': 15.0000, 'Dimension': 1.0000, 'Lacunarity': 3.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ mix_2 = nw.new_node(Nodes.Mix, input_kwargs={6: musgrave_texture_1}, attrs={'data_type': 'RGBA'})
+
+ hue_saturation_value_1 = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Value': 0.6000, 'Color': group_input_1.outputs["Base Color"]})
+
+ multiply_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["Scale"], 1: 20.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input_1.outputs["Seed"], 'Scale': multiply_4, 'Detail': 15.0000, 'Distortion': 0.2000},
+ attrs={'noise_dimensions': '4D'})
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["Scale"], 1: 20.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'W': group_input_1.outputs["Seed"], 'Scale': multiply_5, 'Detail': 15.0000, 'Dimension': 0.2000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: noise_texture.outputs["Fac"], 7: musgrave_texture},
+ attrs={'data_type': 'RGBA'})
+
+ hue_saturation_value = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Value': 1.4000, 'Color': group_input_1.outputs["Base Color"]})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: mix.outputs[2], 6: group_input_1.outputs["Base Color"], 7: hue_saturation_value},
+ attrs={'data_type': 'RGBA'})
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: mix_2.outputs[2], 6: hue_saturation_value_1, 7: mix_1.outputs[2]},
+ attrs={'data_type': 'RGBA'})
+
+ hue_saturation_value_2 = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Value': noise_texture_1.outputs["Fac"], 'Fac': 0.2000, 'Color': mix_3.outputs[2]})
+
+ hue_saturation_value_3 = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Value': 0.2000, 'Color': group_input_1.outputs["Base Color"]})
+
+ mix_6 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: map_range_3.outputs["Result"], 6: hue_saturation_value_2, 7: hue_saturation_value_3},
+ attrs={'data_type': 'RGBA'})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': mix_6.outputs[2], 'Roughness': group_input_1.outputs["Roughness"]})
+
+ multiply_6 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["Crack Amount"], 1: 0.6000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: 5.0000}, attrs={'operation': 'MULTIPLY'})
+
+ group_1 = nw.new_node(nodegroup_crack().name,
+ input_kwargs={'Seed': group_input_1.outputs["Seed"], 'Amount': multiply_6, 'Scale': multiply_7})
+
+ multiply_8 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["Roughness"], 1: 1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_9 = nw.new_node(Nodes.Math, input_kwargs={0: group_1, 1: multiply_8}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_8, 1: group}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_9, 1: multiply_10})
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 0.3000
+
+ multiply_11 = nw.new_node(Nodes.Math,
+ input_kwargs={0: value, 1: group_input_1.outputs["Roughness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_12 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_11, 1: mix_1.outputs[2]}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: multiply_12})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': add_1},
+ attrs={'is_active_output': True})
+
+def shader_concrete(nw: NodeWrangler, scale=1.0, base_color=None, seed=None,
+ roughness=None, crack_amount=None, crack_scale=None, snake_crack=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if roughness is None:
+ roughness = uniform(0.5, 1.0)
+ if crack_amount is None:
+ crack_amount = uniform(0.2, 0.8)
+ if crack_scale is None:
+ crack_scale = uniform(1.0, 3.0)
+ if snake_crack is None:
+ snake_crack = uniform(0.0, 1.0)
+ if base_color is None:
+ base_color = color_category('concrete')
+
+ group = nw.new_node(nodegroup_concrete().name,
+ input_kwargs={'Base Color': base_color, 'Scale': scale, 'Seed': seed,
+ 'Roughness': roughness, 'Crack Amount': crack_amount,
+ 'Crack Scale': crack_scale, 'Snake Crack': snake_crack})
+
+ displacement_1 = nw.new_node('ShaderNodeDisplacement',
+ input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000, 'Scale': 0.0500})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement_1},
+ attrs={'is_active_output': True})
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_concrete, selection=selection, **kwargs)
diff --git a/infinigen/assets/materials/table_marble.py b/infinigen/assets/materials/table_marble.py
new file mode 100644
index 000000000..740f9ef75
--- /dev/null
+++ b/infinigen/assets/materials/table_marble.py
@@ -0,0 +1,158 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=wTzk9T06gdw by Ryan King Arts
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+
+def shader_marble(nw: NodeWrangler,**kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: texture_coordinate.outputs["Object"]}, attrs={'operation': 'SCALE'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': scale.outputs["Vector"]},
+ attrs={'rotation_type': 'EULER_XYZ'})
+
+ seed = nw.new_node(Nodes.Value, label='seed')
+ seed.outputs[0].default_value = 0.0000
+
+ scale_1 = nw.new_node(Nodes.Value, label='scale')
+ scale_1.outputs[0].default_value = 3.0000
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: scale_1, 1: 1.0000})
+
+ noise_texture_2 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': vector_rotate, 'W': seed, 'Scale': add, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_2.outputs["Fac"], 1: 0.4800, 2: 0.6000})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': vector_rotate, 'W': seed, 'Scale': scale_1, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Color"], 'Scale': 8.0000, 'Detail': 15.0000})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture_3.outputs["Color"], 'W': 1.6400, 'Scale': 3.0000},
+ attrs={'feature': 'DISTANCE_TO_EDGE', 'voronoi_dimensions': '4D'})
+
+ colorramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': voronoi_texture.outputs["Distance"]})
+ colorramp_1.color_ramp.elements[0].position = 0.0000
+ colorramp_1.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ colorramp_1.color_ramp.elements[1].position = 0.0300
+ colorramp_1.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: map_range.outputs["Result"], 1: colorramp_1.outputs["Color"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Color"], 'W': seed, 'Scale': 8.0000, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.8000, 6: noise_texture.outputs["Fac"], 7: noise_texture_1.outputs["Fac"]},
+ attrs={'data_type': 'RGBA'})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_1.outputs[2]})
+ colorramp.color_ramp.elements[0].position = 0.3000
+ colorramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ colorramp.color_ramp.elements[1].position = 0.9000
+ colorramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: multiply, 6: colorramp.outputs["Color"], 7: (0.0376, 0.0179, 0.0033, 1.0000)},
+ attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node('ShaderNodeBump', input_kwargs={'Strength': 0.0200, 'Height': multiply})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': mix_1.outputs[2], 'Specular': 0.6000, 'Roughness': 0.1000, 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+def shader_wood(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: texture_coordinate.outputs["Object"]}, attrs={'operation': 'SCALE'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': scale.outputs["Vector"]},
+ attrs={'rotation_type': 'EULER_XYZ'})
+
+ mapping_2 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate, 'Scale': (5.0000, 100.0000, 100.0000)})
+
+ seed = nw.new_node(Nodes.Value, label='seed')
+ seed.outputs[0].default_value = 0.0000
+
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': mapping_2, 'W': seed, 'Scale': 10.0000, 'Detail': 15.0000, 'Dimension': 7.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_2, 3: 1.0000, 4: -1.0000})
+
+ mapping_1 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'W': seed, 'Scale': 0.5000, 'Detail': 1.0000, 'Distortion': 1.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'W': seed, 'Scale': noise_texture_1.outputs["Fac"], 'Detail': 15.0000, 'Dimension': 0.2000, 'Lacunarity': 2.4000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_1, 3: -1.4000, 4: 1.5000})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': map_range.outputs["Result"], 3: 1.0000, 4: 0.5000})
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate, 'Scale': (0.1500, 1.0000, 0.1500)})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': seed, 'Detail': 5.0000, 'Distortion': 1.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'W': seed, 'Scale': 4.0000, 'Detail': 10.0000, 'Dimension': 0.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: noise_texture.outputs["Fac"], 7: musgrave_texture},
+ attrs={'data_type': 'RGBA'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: map_range_1.outputs["Result"], 7: mix.outputs[2]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9500, 6: map_range_2.outputs["Result"], 7: mix_1.outputs[2]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = (0.0242, 0.0056, 0.0027, 1.0000)
+
+ rgb_1 = nw.new_node(Nodes.RGB)
+ rgb_1.outputs[0].default_value = (0.5089, 0.2122, 0.0685, 1.0000)
+
+ mix_3 = nw.new_node(Nodes.Mix, input_kwargs={0: mix_2.outputs[2], 6: rgb, 7: rgb_1}, attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node('ShaderNodeBump', input_kwargs={'Strength': 0.2000, 'Height': mix_2.outputs[2]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': mix_3.outputs[2], 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/table_materials.py b/infinigen/assets/materials/table_materials.py
new file mode 100644
index 000000000..96c8b5756
--- /dev/null
+++ b/infinigen/assets/materials/table_materials.py
@@ -0,0 +1,163 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba, rgb2hsv
+from infinigen.core import surface
+from infinigen.core.util.random import log_uniform
+
+
+def shader_marble(nw: NodeWrangler,**kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: texture_coordinate.outputs["Object"]}, attrs={'operation': 'SCALE'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': scale.outputs["Vector"]},
+ attrs={'rotation_type': 'EULER_XYZ'})
+
+ seed = nw.new_node(Nodes.Value, label='seed')
+ seed.outputs[0].default_value = 0.0000
+
+ scale_1 = nw.new_node(Nodes.Value, label='scale')
+ scale_1.outputs[0].default_value = 3.0000
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: scale_1, 1: 1.0000})
+
+ noise_texture_2 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': vector_rotate, 'W': seed, 'Scale': add, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': noise_texture_2.outputs["Fac"], 1: 0.4800, 2: 0.6000})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': vector_rotate, 'W': seed, 'Scale': scale_1, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'Scale': 8.0000, 'Detail': 15.0000})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': noise_texture_3.outputs["Fac"], 'W': 1.6400, 'Scale': 3.0000},
+ attrs={'feature': 'DISTANCE_TO_EDGE', 'voronoi_dimensions': '4D'})
+
+ colorramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': voronoi_texture.outputs["Distance"]})
+ colorramp_1.color_ramp.elements[0].position = 0.0000
+ colorramp_1.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ colorramp_1.color_ramp.elements[1].position = 0.0300
+ colorramp_1.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: map_range.outputs["Result"], 1: colorramp_1.outputs["Color"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'W': seed, 'Scale': 8.0000, 'Detail': 15.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.8000, 6: noise_texture.outputs["Fac"], 7: noise_texture_1.outputs["Fac"]},
+ attrs={'data_type': 'RGBA'})
+
+ colorramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': mix_1.outputs[2]})
+ colorramp.color_ramp.elements[0].position = 0.3000
+ colorramp.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ colorramp.color_ramp.elements[1].position = 0.9000
+ colorramp.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: multiply, 6: colorramp.outputs["Color"], 7: (0.0376, 0.0179, 0.0033, 1.0000)},
+ attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node('ShaderNodeBump', input_kwargs={'Strength': 0.0200, 'Height': multiply})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': mix_1.outputs[2], 'Specular': 0.6000, 'Roughness': 0.1000, 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+def perturb(hsv):
+ return np.array([hsv[0]+uniform(-.02,.02), hsv[1]+uniform(-.2,.2), hsv[2]*log_uniform(.5,2.)])
+
+def shader_wood(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: texture_coordinate.outputs["Object"]}, attrs={'operation': 'SCALE'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': scale.outputs["Vector"]},
+ attrs={'rotation_type': 'EULER_XYZ'})
+
+ mapping_2 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate, 'Scale': (5.0000, 100.0000, 100.0000)})
+
+ seed = nw.new_node(Nodes.Value, label='seed')
+ seed.outputs[0].default_value = 0.0000
+
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': mapping_2, 'W': seed, 'Scale': 10.0000, 'Detail': 15.0000, 'Dimension': 7.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_2, 3: 1.0000, 4: -1.0000})
+
+ mapping_1 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'W': seed, 'Scale': 0.5000, 'Detail': 1.0000, 'Distortion': 1.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'W': seed, 'Scale': noise_texture_1.outputs["Fac"], 'Detail': 15.0000, 'Dimension': 0.2000, 'Lacunarity': 2.4000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_1, 3: -1.4000, 4: 1.5000})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': map_range.outputs["Result"], 3: 1.0000, 4: 0.5000})
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vector_rotate, 'Scale': (0.1500, 1.0000, 0.1500)})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': seed, 'Detail': 5.0000, 'Distortion': 1.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'W': seed, 'Scale': 4.0000, 'Detail': 10.0000, 'Dimension': 0.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: noise_texture.outputs["Fac"], 7: musgrave_texture},
+ attrs={'data_type': 'RGBA'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: map_range_1.outputs["Result"], 7: mix.outputs[2]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9500, 6: map_range_2.outputs["Result"], 7: mix_1.outputs[2]},
+ attrs={'data_type': 'RGBA', 'blend_type': 'MULTIPLY'})
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = hsv2rgba(perturb(rgb2hsv(0.0242, 0.0056, 0.0027)))
+
+ rgb_1 = nw.new_node(Nodes.RGB)
+ rgb_1.outputs[0].default_value = hsv2rgba(perturb(rgb2hsv(0.5089, 0.2122, 0.0685)))
+
+ mix_3 = nw.new_node(Nodes.Mix, input_kwargs={0: mix_2.outputs[2], 6: rgb, 7: rgb_1}, attrs={'data_type': 'RGBA'})
+
+ bump = nw.new_node('ShaderNodeBump', input_kwargs={'Strength': 0.2000, 'Height': mix_2.outputs[2]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': mix_3.outputs[2], 'Normal': bump})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/materials/text.py b/infinigen/assets/materials/text.py
new file mode 100644
index 000000000..fe7ee649a
--- /dev/null
+++ b/infinigen/assets/materials/text.py
@@ -0,0 +1,309 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Lingjie Mei: text & art generators
+# - Stamatis Alexandropoulos: image postprocessing effects
+# Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=hpamCaVrbTk by Joey Carlino
+
+import colorsys
+import inspect
+import io
+import string
+import logging
+import colorsys
+
+import matplotlib.font_manager
+import matplotlib.pyplot as plt
+from matplotlib.patches import Arrow, BoxStyle, Circle, Ellipse, FancyBboxPatch, Rectangle, RegularPolygon, Wedge
+
+import bpy
+import numpy as np
+from PIL import Image
+from numpy.random import uniform, rand
+
+from infinigen.assets.utils.decorate import decimate
+from infinigen.assets.utils.misc import generate_text
+from infinigen.assets.utils.object import new_plane
+from infinigen.assets.utils.uv import compute_uv_direction
+from infinigen.assets.materials import common
+
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+from infinigen.core.util.math import FixedSeed, clip_gaussian
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.random import random_general as rg
+
+logger = logging.getLogger(__name__)
+
+class Text:
+ font_names_all = matplotlib.font_manager.get_font_names()
+ default_font_name = 'DejaVu Sans'
+ patch_fns = 'weighted_choice', (2, Circle), (4, Rectangle), (1, Wedge), (1, RegularPolygon), (1, Ellipse), (
+ 2, Arrow), (2, FancyBboxPatch)
+ hatches = {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
+ font_weights = ['normal', 'bold', 'heavy']
+ font_styles = ['normal', 'italic', 'oblique']
+
+ def __init__(self, factory_seed, has_barcode=True, emission=0):
+ self.factory_seed = factory_seed
+ with FixedSeed(self.factory_seed):
+ self.size = 4
+ self.dpi = 100
+ self.colormap = self.build_sequential_colormap() if uniform() < .5 else \
+ self.build_diverging_colormap()
+ self.white_chance = .03
+ self.black_chance = .05
+
+ self.n_patches = np.random.randint(5, 8)
+ self.force_horizontal = uniform() < .75
+
+ self.font_names = np.random.choice(self.font_names_all, 3)
+ self.n_texts = np.random.randint(2, 4)
+
+ self.n_barcodes = 1 if has_barcode and uniform() < .5 else 0
+ self.barcode_scale = uniform(.3, .6)
+ self.barcode_length = np.random.randint(25, 40)
+ self.barcode_aspect = log_uniform(1.5, 3)
+
+ self.emission = emission
+
+ @staticmethod
+ def build_diverging_colormap():
+ count = 20
+ hue = (uniform() + np.linspace(0, .5, count)) % 1
+ mid = uniform(.6, .8)
+ lightness = np.concatenate(
+ [np.linspace(uniform(.1, .3), mid, count // 2), np.linspace(mid, uniform(.1, .3), count // 2)]
+ )
+ saturation = np.concatenate([np.linspace(1, .5, count // 2), np.linspace(.5, 1, count // 2)])
+
+ # TODO hack
+ saturation *= uniform(0, 1)
+ lightness *= uniform(0.5, 1)
+
+ return np.array([colorsys.hls_to_rgb(h, l, s) for h, l, s in zip(hue, lightness, saturation)])
+
+ @staticmethod
+ def build_sequential_colormap():
+ count = 20
+ hue = (uniform() + np.linspace(0, .5, count)) % 1
+ lightness = np.linspace(uniform(.0), uniform(.6, .8), count)
+ saturation = np.concatenate([np.linspace(1, .5, count // 2), np.linspace(.5, 1, count // 2)])
+
+ # TODO hack
+ saturation *= uniform(0, 1)
+ lightness *= uniform(0.5, 1)
+
+ return np.array([colorsys.hls_to_rgb(h, l, s) for h, l, s in zip(hue, lightness, saturation)])
+
+ @property
+ def random_color(self):
+ r = uniform()
+ if r < self.white_chance:
+ return np.array([1, 1, 1])
+ elif r < self.white_chance + self.black_chance:
+ return np.array([0, 0, 0])
+ else:
+ return self.colormap[np.random.randint(len(self.colormap))]
+
+ @property
+ def random_colors(self):
+ while True:
+ c, d = self.random_color, self.random_color
+ if np.abs(c - d).sum() > .2:
+ return c, d
+
+ def build_image(self, bbox):
+ fig = plt.figure(figsize=(self.size, self.size), dpi=self.dpi)
+ ax = fig.add_axes((0, 0, 1, 1))
+ ax.set_facecolor(self.random_color)
+ locs = self.get_locs(bbox, self.n_patches + self.n_texts + self.n_barcodes)
+ self.add_divider(bbox)
+ self.add_patches(locs[:self.n_patches], bbox)
+ self.add_texts(locs[self.n_patches:self.n_patches + self.n_texts])
+ self.add_barcodes(locs[self.n_patches + self.n_texts:])
+ buffer = io.BytesIO()
+ fig.savefig(buffer, format='png')
+ buffer.seek(0)
+ size = self.size * self.dpi
+ image = bpy.data.images.new('text_texture', width=size, height=size, alpha=True)
+ data = np.asarray(Image.open(buffer), dtype=np.float32)[::-1, :] / 255.
+ image.pixels.foreach_set(data.ravel())
+ image.pack()
+ plt.close('all')
+ plt.clf()
+ return image
+
+ @staticmethod
+ def loc_uniform(min_, max_, size=None):
+ ratio = .1
+ return uniform(min_ + ratio * (max_ - min_), min_ + (1 - ratio) * (max_ - min_), size)
+
+ @staticmethod
+ def scale_uniform(min_, max_):
+ return (max_ - min_) * log_uniform(.2, .8)
+
+ def get_locs(self, bbox, n):
+ m = 8 * n
+ x, y = self.loc_uniform(bbox[0], bbox[1], m), self.loc_uniform(bbox[2], bbox[3], m)
+ return decimate(np.stack([x, y], -1), n)
+
+ def add_divider(self, rs):
+ if uniform() < .6: return
+ a = 0 if uniform() < .7 else uniform(5, 10)
+ x, y = self.loc_uniform(rs[0], rs[1]), self.loc_uniform(rs[2], rs[3])
+ if rs[0] == 0 or self.force_horizontal:
+ args_list = [[(0, y), 2, 2, a], [(0, y), 2, -2, -a], [(1, y), -2, -2, a], [(1, y), -2, 2, -a]]
+ else:
+ args_list = [[(x, 0), -2, 2, a], [(x, 0), 2, 2, -a], [(x, 1), 2, -2, a], [(x, 1), -2, -2, -a]]
+ args = args_list[np.random.randint(len(args_list))]
+ plt.gca().add_patch(Rectangle(*args[:-1], angle=args[-1], color=self.random_color))
+
+ def add_patches(self, locs, bbox):
+ for x, y in locs:
+ w, h = self.scale_uniform(bbox[0], bbox[1]), self.scale_uniform(bbox[2], bbox[3])
+ x_, y_ = x - w / 2, y - h / 2
+ r = min(w, h) / 2
+ fn = rg(self.patch_fns)
+ kwargs = {
+ 'alpha': uniform(.5, .8) if uniform() < .2 else 1,
+ 'fill': uniform() < .2,
+ 'angle': 0 if uniform() < .8 else uniform(-30, 30),
+ 'orientation': uniform(0, np.pi * 2)
+ }
+ kwargs = {k: kwargs[k] for k, v in inspect.signature(fn).parameters.items() if k in kwargs}
+ face_color, edge_color = self.random_colors
+ kwargs.update(
+ {
+ 'facecolor': face_color, 'edgecolor': edge_color,
+ 'hatch': np.random.choice(list(self.hatches)) if uniform() < .3 else 'none',
+ 'linewidth': uniform(2, 5)
+ }
+ )
+ match fn.__name__:
+ case Circle.__name__:
+ patch = Circle((x, y), r, **kwargs)
+ case Rectangle.__name__:
+ patch = Rectangle((x_, y_), w, h, **kwargs)
+ case Wedge.__name__:
+ start = uniform(0, 360)
+ patch = Wedge((x, y), r, start, start + uniform(0, 360), width=uniform(.2, .8) * r, **kwargs)
+ case RegularPolygon.__name__:
+ patch = RegularPolygon((x, y), np.random.randint(3, 9), radius=r, **kwargs)
+ case Ellipse.__name__:
+ patch = Ellipse((x, y), w, h, **kwargs)
+ case Arrow.__name__:
+ w_, h_ = (w if uniform() < .5 else -w), (h if uniform() < .5 else -h)
+ patch = Arrow(x - w_ / 2, y - h_ / 2, w, h, width=log_uniform(.6, 1.5), **kwargs)
+ case FancyBboxPatch.__name__:
+ pad = uniform(.2, .4) * min(w, h)
+ box_style = np.random.choice(list(BoxStyle.get_styles().values()))(pad=pad)
+ patch = FancyBboxPatch(
+ (x_, y_), w - pad, h - pad, box_style, mutation_scale=log_uniform(.6, 1.5),
+ mutation_aspect=log_uniform(.6, 1.5), **kwargs
+ )
+ case _:
+ raise NotImplementedError
+ try:
+ plt.gca().add_patch(patch)
+ except MemoryError:
+ logger.warning(f'Failed to add patch {fn.__name__} at {x, y} with {w, h} due to MemoryError')
+
+ def add_texts(self, locs):
+ for x, y in locs:
+ x = .5 + (x - .5) * .6
+ text = generate_text()
+ family = np.random.permutation(self.font_names).tolist() + ['DejaVu Sans']
+ color, background_color = self.random_colors
+ plt.figtext(
+ x, y, text, family=family,
+ size=log_uniform(.75, 1) * self.dpi * clip_gaussian(0.3, 0.2, 0.2, 0.65),
+ ha='center', va='center', c=color,
+ rotation=uniform(-10, 10), wrap=True,
+ fontweight=np.random.choice(self.font_weights),
+ fontstyle=np.random.choice(self.font_styles),
+ backgroundcolor=background_color
+ )
+
+ def add_barcodes(self, locs):
+ fig = plt.gcf()
+ for x, y in locs:
+ code = np.random.randint(0, 2, self.barcode_length)
+ h = self.barcode_scale / self.size
+ w = h * self.barcode_aspect
+ ax = fig.add_axes((x - w / 2, y - h / 2, w, h))
+ ax.set_axis_off()
+ ax.imshow(code.reshape(1, -1), cmap='binary', aspect='auto', interpolation='nearest')
+
+ def make_shader_func(self, bbox):
+ assert bbox[1] - bbox[0] > .001 and bbox[3] - bbox[2] >.001
+ image = self.build_image(bbox)
+
+ def shader_text(nw: NodeWrangler, **kwargs):
+ uv_map = nw.new_node(Nodes.UVMap)
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': uv_map})
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture, input_kwargs={'Vector': reroute, 'Scale': 60.0000})
+
+ voronoi_texture_1 = nw.new_node(Nodes.VoronoiTexture, input_kwargs={'Vector': reroute, 'Scale': 60.0000})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: voronoi_texture.outputs["Position"], 7: voronoi_texture_1.outputs["Position"]},
+ attrs={'data_type': 'RGBA'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture, input_kwargs={'Vector': reroute, 'Detail': 5.6000, 'Dimension': 1.4000})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': reroute, 'Scale': 35.4000, 'Detail': 3.3000, 'Roughness': 1.0000})
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: uniform(0.2,1.0), 6: musgrave_texture, 7: noise_texture_1.outputs["Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ mix_1 = nw.new_node(Nodes.Mix, input_kwargs={0: 0.0417, 6: mix.outputs[2], 7: mix_3.outputs[2]}, attrs={'data_type': 'RGBA'})
+
+ if rand() < 0.5:
+ mix_2 = nw.new_node(Nodes.Mix, input_kwargs={0: uniform(0, 0.4), 6: mix_1.outputs[2], 7: uv_map}, attrs={'data_type': 'RGBA'})
+ else:
+ mix_2 = nw.new_node(Nodes.Mix, input_kwargs={0: 1.0, 6: mix_1.outputs[2], 7: uv_map}, attrs={'data_type': 'RGBA'})
+ # mix_2 = nw.new_node(Nodes.Mix, input_kwargs={0: 0.7375, 6: uv, 7: mix_1.outputs[2]}, attrs={'data_type': 'RGBA'})
+ color = nw.new_node(Nodes.ShaderImageTexture, [mix_2], attrs={'image': image}).outputs[0]
+ roughness = nw.new_node(Nodes.NoiseTexture)
+ if self.emission > 0:
+ emission = color
+ color = (0.05, 0.05, 0.05, 1)
+ roughness = 0.05
+ else:
+ emission = None
+ principled_bsdf = nw.new_node(
+ Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': color,
+ 'Roughness': roughness,
+ 'Metallic': uniform(0, .5),
+ 'Specular': uniform(0, .2),
+ 'Emission': emission,
+ 'Emission Strength': self.emission
+ }
+ )
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf})
+
+ return shader_text
+
+ def apply(self, obj, selection=None, bbox=(0, 1, 0, 1), **kwargs):
+ common.apply(obj, self.make_shader_func(bbox), selection, **kwargs)
+
+
+def apply(obj, selection=None, bbox=(0, 1, 0, 1), has_barcode=True, emission=0, **kwargs):
+ Text(np.random.randint(1e5), has_barcode, emission).apply(obj, selection, bbox, **kwargs)
+
+
+def make_sphere():
+ obj = new_plane()
+ obj.rotation_euler[0] = np.pi / 2
+ butil.apply_transform(obj)
+ compute_uv_direction(obj, 'x', 'z')
+ return obj
diff --git a/infinigen/assets/materials/text_no_barcode.py b/infinigen/assets/materials/text_no_barcode.py
new file mode 100644
index 000000000..51021c598
--- /dev/null
+++ b/infinigen/assets/materials/text_no_barcode.py
@@ -0,0 +1,12 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+
+from .text import Text
+from .text import make_sphere
+
+
+def apply(obj, selection=None, bbox=(0, 1, 0, 1), emission=0, **kwargs):
+ Text(np.random.randint(1e5), False, emission).apply(obj, selection, bbox, **kwargs)
diff --git a/infinigen/assets/materials/tile.py b/infinigen/assets/materials/tile.py
new file mode 100644
index 000000000..a926f67d9
--- /dev/null
+++ b/infinigen/assets/materials/tile.py
@@ -0,0 +1,346 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from inspect import signature
+
+import numpy as np
+from numpy.random import uniform
+
+from . import ceramic, common
+from .utils.surface_utils import perturb_coordinates
+from ..utils.object import new_cube
+
+from ...core.nodes import NodeWrangler, Nodes
+from ...core.util.math import FixedSeed
+from ...core.util.random import log_uniform
+
+from functools import partial
+
+
+def mix_shader(nw, base_shader, offset, rotations, mortar, alternating, selections):
+ n = len(selections) + 1
+ seeds = np.random.randint(0, 1e7, n) if alternating else [np.random.randint(1e7)] * n
+ shaders, disps = [], []
+ darken_factor = uniform(.4, 1.)
+ for i, seed in enumerate(seeds):
+ with FixedSeed(seed):
+ kwargs = {}
+ names = signature(base_shader).parameters
+ if 'random_seed' in names:
+ kwargs['random_seed'] = np.random.randint(1e7)
+ if 'w' in names:
+ kwargs['w'] = offset
+ if 'hscale' in names:
+ if i % 2 == 0:
+ kwargs['hscale'] = log_uniform(20, 30)
+ kwargs['vscale'] = .01
+ else:
+ kwargs['hscale'] = .01
+ kwargs['vscale'] = log_uniform(20, 30)
+ base_shader(nw, **kwargs)
+ bsdfs = nw.find('Bsdf')
+ n = nw.nodes[-1]
+ if len(bsdfs) > 0:
+ bsdf = bsdfs[-1]
+ links = nw.find_from(bsdf.inputs[0])
+ if len(links) > 0:
+ color = links[0].from_socket
+ else:
+ color = bsdf.inputs[0].default_value
+ color = nw.new_node(Nodes.MixRGB,
+ input_kwargs={0: darken_factor, 6: color, 7: nw.scalar_sub(1, mortar)},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'}).outputs[2]
+ nw.connect_input(color, bsdf.inputs[0])
+ match type(n).__name__:
+ case Nodes.GroupOutput:
+ shaders.append(nw.find_from(n.inputs[0])[0].from_socket)
+ disp_links = nw.find_from(n.inputs[1])
+ disps.append(disp_links[0].from_socket if len(disp_links) > 0 else None)
+ nw.nodes.remove(n)
+ case Nodes.PrincipledBSDF | Nodes.GlassBSDF | Nodes.GlossyBSDF | Nodes.TranslucentBSDF | \
+ Nodes.TransparentBSDF | Nodes.TranslucentBSDF:
+ shaders.append(n.outputs[0])
+ disps.append(None)
+ case _:
+ n = nw.find(Nodes.MaterialOutput)[-1]
+ shaders.append(nw.find_from(n.inputs['Surface'])[0].from_socket)
+ disp_links = nw.find_from(n.inputs['Displacement'])
+ disps.append(disp_links[0].from_socket if len(disp_links) > 0 else None)
+ shader = shaders[0]
+ disp = disps[0]
+ rotation = rotations[0]
+ for sel, sh, dis, rot in zip(selections, shaders[1:], disps[1:], rotations[1:]):
+ shader = nw.new_node(Nodes.MixShader, [sel, shader, sh])
+ disp = nw.new_node(Nodes.Mix, input_kwargs={'Factor': sel, 'A': disp, 'B': dis},
+ attrs={'data_type': 'VECTOR'})
+ rotation = nw.new_node(Nodes.Mix, input_kwargs={'Factor': sel, 'A': rotation, 'B': rot},
+ attrs={'data_type': 'FLOAT'})
+ for node in nw.find(Nodes.TextureCoord)[1:] + nw.find(Nodes.NewGeometry):
+ perturb_coordinates(nw, node, offset, rotation)
+ disp = nw.add(disp, nw.new_node(Nodes.Displacement, input_kwargs={
+ 'Height': nw.scalar_multiply(mortar, -uniform(.01, .02)),
+ 'Midlevel': 0.
+ }))
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': shader, 'Displacement': disp})
+
+
+def shader_square_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1, **kwargs):
+ if alternating is None:
+ alternating = uniform() < .75
+ size = log_uniform(.2, .4)
+ vec = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ rotation = np.pi / 4 if uniform() < .3 else 0
+ vec = nw.new_node(
+ Nodes.Mapping,
+ [vec, uniform(0, 1, 3), (0, 0, np.pi / 2 * np.random.randint(4) + rotation), [scale] * 3]
+ )
+ vec = nw.combine(*nw.separate(vec)[:2], 0)
+ offset, mortar = map(nw.new_node(Nodes.BrickTexture, [vec], input_kwargs={
+ 'Scale': 1 / size,
+ 'Row Height': 1,
+ 'Brick Width': 1,
+ 'Mortar Size': uniform(.005, .01),
+ 'Color2': (0, 0, 0, 1)
+ }, attrs={'offset': .0, 'offset_frequency': 1}).outputs.get, ['Color', 'Fac'])
+ selections = [nw.new_node(Nodes.CheckerTexture, [vec, (0, 0, 0, 1), (1, 1, 1, 1), 1 / size]).outputs[1]]
+ rotations = np.pi / 2 * np.arange(2)
+ mix_shader(nw, base_shader, offset, rotations, mortar, alternating, selections)
+
+
+def shader_rectangle_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1, **kwargs):
+ if alternating is None:
+ alternating = uniform() < .75
+ size = log_uniform(.2, .4)
+ vec = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ vec = nw.new_node(
+ Nodes.Mapping,
+ [vec, uniform(0, 1, 3), (0, 0, np.pi / 2 * np.random.randint(4)), [scale, scale * log_uniform(1.3, 2), scale]]
+ )
+ vec = nw.combine(*nw.separate(vec)[:2], 0)
+ offset, mortar = map(nw.new_node(Nodes.BrickTexture, [vec], input_kwargs={
+ 'Scale': 1 / size,
+ 'Row Height': 1,
+ 'Brick Width': 1,
+ 'Mortar Size': uniform(.005, .01),
+ 'Color2': (0, 0, 0, 1)
+ }, attrs={'offset': .0, 'offset_frequency': 1}).outputs.get, ['Color', 'Fac'])
+ selections = [nw.new_node(Nodes.CheckerTexture, [vec, (0, 0, 0, 1), (1, 1, 1, 1), 1 / size]).outputs[1]]
+ rotations = np.pi / 2 * np.arange(2)
+ mix_shader(nw, base_shader, offset, rotations, mortar, alternating, selections)
+
+
+def shader_hexagon_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1, **kwargs):
+ if alternating is None:
+ alternating = uniform() < .6
+ size = log_uniform(.15, .3)
+ vec = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ vec = nw.new_node(Nodes.Mapping,
+ [vec, uniform(0, 1, 3), (0, 0, np.pi / 2 * np.random.randint(4)), [scale] * 3])
+ qs = []
+ for n in np.array([[1 / np.sqrt(3), -1 / 3, 0], [0, 2 / 3, 0], [-1 / np.sqrt(3), -1 / 3, 0]]) / size:
+ qs.append(nw.vector_math('DOT_PRODUCT', vec, n))
+ qs_ = [nw.math('ROUND', q) for q in qs]
+ qs_diff = [nw.math('ABSOLUTE', nw.scalar_sub(q, q_)) for q, q_ in zip(qs, qs_)]
+ coords = []
+ for i in range(3):
+ coords.append(nw.new_node(Nodes.Mix, [
+ nw.scalar_multiply(nw.math('GREATER_THAN', qs_diff[i], qs_diff[(i + 1) % 3]),
+ nw.math('GREATER_THAN', qs_diff[i], qs_diff[(i + 2) % 3])), None, qs_[i],
+ nw.scalar_sub(0, nw.scalar_add(qs_[(i + 1) % 3], qs_[(i + 2) % 3]))]))
+ offset = nw.combine(coords[0], coords[1], coords[2])
+ i = np.random.randint(3)
+ fraction = nw.math('FRACT',
+ nw.scalar_divide(nw.scalar_add(nw.scalar_sub(coords[i], coords[(i + 1) % 3]), .5), 3))
+ diffs = [nw.math('ABSOLUTE', nw.scalar_sub(q, c)) for q, c in zip(qs, coords)]
+ max_dist = nw.math('MAXIMUM',
+ nw.math('MAXIMUM', nw.scalar_add(diffs[0], diffs[1]), nw.scalar_add(diffs[1], diffs[2])),
+ nw.scalar_add(diffs[2], diffs[0]))
+ mortar = nw.math('GREATER_THAN', max_dist, 1 - uniform(.005, .01) / size / 2)
+ rotations = np.pi * 2 / 3 * np.arange(3)
+ mix_shader(nw, base_shader, offset, rotations, mortar, alternating,
+ [nw.math('LESS_THAN', fraction, 2 / 3), nw.math('LESS_THAN', fraction, 1 / 3), ])
+
+
+def shader_staggered_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1,
+ vertical_scale=None, **kwargs):
+ horizontal_scale = scale * log_uniform(2., 3.5)
+ if vertical_scale is None:
+ vertical_scale = horizontal_scale * log_uniform(0.05, 0.2)
+
+ vec = nw.new_node(Nodes.TextureCoord).outputs["Object"]
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ vec = nw.new_node(Nodes.Mapping, [vec, uniform(0, 1, 3)])
+ vec = nw.add(vec, nw.combine(0, nw.scalar_divide(.5, horizontal_scale), 0))
+
+ offset, mortar = map(nw.new_node(Nodes.BrickTexture, input_kwargs={
+ 'Vector': vec,
+ 'Color2': (0, 0, 0, 1.0000),
+ 'Scale': 1.0000,
+ 'Mortar Size': uniform(.005, .01),
+ 'Mortar Smooth': 1.0000,
+ 'Bias': -0.5000,
+ 'Brick Width': nw.scalar_divide(1, vertical_scale),
+ 'Row Height': nw.scalar_divide(1, horizontal_scale)
+ }, attrs={'squash_frequency': 1}).outputs.get, ['Color', 'Fac'])
+ mix_shader(nw, base_shader, offset, [0], mortar, alternating, [])
+
+
+def shader_crossed_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1, n=None,
+ **kwargs):
+ n = np.random.randint(4, 8)
+ vec = nw.new_node(Nodes.TextureCoord).outputs["Object"]
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ vec = nw.new_node(Nodes.Mapping,
+ [vec, uniform(0, 1, 3), (0, 0, np.pi / 2 * np.random.randint(4)), [scale] * 3])
+ x, y, z = nw.separate(vec)
+ x_ = nw.scalar_sub(x, nw.scalar_divide(nw.math('FLOOR', nw.scalar_multiply(y, n)), n))
+ vec = nw.combine(x_, y, 0)
+ offset, mortar = map(nw.new_node(Nodes.BrickTexture, input_kwargs={
+ 'Vector': vec,
+ 'Color2': (0, 0, 0, 1.0000),
+ 'Scale': 1.0000,
+ 'Mortar Size': uniform(.005, .01),
+ 'Brick Width': 1,
+ 'Row Height': 1 / n
+ }, attrs={'squash_frequency': 1, 'offset': 0}).outputs.get, ['Color', 'Fac'])
+ vec_ = nw.combine(
+ nw.scalar_sub(y, nw.scalar_divide(nw.scalar_add(nw.math('FLOOR', nw.scalar_multiply(x, n)), 1), n)),
+ nw.scalar_sub(0, x), 0)
+
+ offset_, mortar_ = map(nw.new_node(Nodes.BrickTexture, input_kwargs={
+ 'Vector': vec_,
+ 'Color2': (0, 0, 0, 1.0000),
+ 'Scale': 1.0000,
+ 'Mortar Size': uniform(.005, .01),
+ 'Brick Width': 1,
+ 'Row Height': 1 / n,
+ }, attrs={'squash_frequency': 1, 'offset': 0}).outputs.get, ['Color', 'Fac'])
+ selection = nw.math('LESS_THAN',
+ nw.scalar_sub(nw.scalar_divide(x_, 2), nw.math('FLOOR', nw.scalar_divide(x_, 2))), .5)
+ offset = nw.new_node(Nodes.Mix, input_kwargs={'Factor': selection, 'A': offset, 'B': offset_},
+ attrs={'data_type': 'FLOAT'})
+ mortar = nw.new_node(Nodes.Mix, input_kwargs={'Factor': selection, 'A': mortar, 'B': mortar_},
+ attrs={'data_type': 'FLOAT'})
+
+ mix_shader(nw, base_shader, offset, [0, np.pi / 2], mortar, alternating, [selection])
+
+
+def shader_composite_tile(nw: NodeWrangler, base_shader, vertical=False, alternating=None, scale=1, **kwargs):
+ if alternating is None:
+ alternating = uniform() < .75
+ size = log_uniform(.2, .4)
+ vec = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ normal = nw.new_node(Nodes.ShaderNodeNormalMap).outputs['Normal']
+ if vertical:
+ vec = nw.combine(nw.separate(nw.vector_math('CROSS_PRODUCT', vec, normal))[-1], nw.separate(vec)[-1], 0)
+ vec = nw.new_node(
+ Nodes.Mapping,
+ [vec, uniform(0, 1, 3), (0, 0, np.pi / 2 * np.random.randint(8)), [scale] * 3]
+ )
+ vec = nw.combine(*nw.separate(vec)[:2], 0)
+
+ selections = [nw.new_node(Nodes.CheckerTexture, [vec, (0, 0, 0, 1), (1, 1, 1, 1), 1 / size]).outputs[1]]
+ rotations = np.pi / 2 * np.arange(2)
+
+ mortar_size = uniform(.002, .005)
+ stride = np.random.randint(4, 7)
+ offset_h, mortar_h = map(
+ nw.new_node(
+ Nodes.BrickTexture, input_kwargs={
+ 'Vector': vec,
+ 'Color2': (0, 0, 0, 1.0000),
+ 'Scale': 1.0000,
+ 'Mortar Size': mortar_size,
+ 'Mortar Smooth': 1.0000,
+ 'Brick Width': size / stride,
+ 'Row Height': 1000
+ }, attrs={'squash_frequency': 1}
+ ).outputs.get, ['Color', 'Fac']
+ )
+ offset_v, mortar_v = map(
+ nw.new_node(
+ Nodes.BrickTexture, input_kwargs={
+ 'Vector': vec,
+ 'Color2': (0, 0, 0, 1.0000),
+ 'Scale': 1.0000,
+ 'Mortar Size': mortar_size,
+ 'Mortar Smooth': 1.0000,
+ 'Brick Width': 1000,
+ 'Row Height': size / stride,
+ }, attrs={'squash_frequency': 1}
+ ).outputs.get, ['Color', 'Fac']
+ )
+ mortar = nw.new_node(
+ Nodes.Mix, input_kwargs={'Factor': selections[0], 'A': mortar_h, 'B': mortar_v},
+ attrs={'data_type': 'FLOAT'}
+ )
+ offset = nw.new_node(
+ Nodes.Mix, input_kwargs={'Factor': selections[0], 'A': offset_h, 'B': offset_v},
+ attrs={'data_type': 'VECTOR'}
+ )
+ mix_shader(nw, base_shader, offset, rotations, mortar, alternating, selections)
+
+
+def get_shader_funcs():
+ from . import bone, ceramic, cobble_stone, dirt, stone
+ from .woods.wood import shader_wood
+ from .table_materials import shader_marble
+ return [(bone.shader_bone, 1), (cobble_stone.shader_cobblestone, 1), (ceramic.shader_ceramic, 4),
+ (dirt.shader_dirt, 1), (stone.shader_stone, 1), (shader_marble, 2), (shader_wood, 5), ]
+
+
+def apply(obj, selection=None, vertical=False, shader_func=None, scale=None, alternating=None, shape=None,
+ **kwargs):
+ funcs, weights = zip(*get_shader_funcs())
+ weights = np.array(weights) / sum(weights)
+ if shader_func is None:
+ shader_func = np.random.choice(funcs, p=weights)
+ name = shader_func.__name__
+
+ if scale is None:
+ scale = log_uniform(1., 2.)
+
+ if shader_func == ceramic.shader_ceramic:
+ low = uniform(.1, .3)
+ high = uniform(.6, .8)
+ shader_func = partial(ceramic.shader_ceramic, roughness_min=low, roughness_max=high)
+ match shape:
+ case 'square':
+ method = shader_square_tile
+ case 'rectangle':
+ method = shader_rectangle_tile
+ case 'hexagon':
+ method = shader_hexagon_tile
+ case 'staggered':
+ method = shader_staggered_tile
+ case 'crossed':
+ method = shader_crossed_tile
+ case 'composite':
+ method = shader_composite_tile
+ case _:
+ method = np.random.choice(
+ [shader_hexagon_tile, shader_square_tile, shader_rectangle_tile, shader_staggered_tile,
+ shader_crossed_tile]
+ )
+
+ return common.apply(
+ obj, method, selection, shader_func, vertical, alternating, name=f'{name}_{method.__name__}_tile',
+ scale=scale, **kwargs)
+
+
+def make_sphere():
+ return new_cube()
\ No newline at end of file
diff --git a/infinigen/assets/materials/utils/surface_utils.py b/infinigen/assets/materials/utils/surface_utils.py
index 996323b5c..d740bcc8c 100644
--- a/infinigen/assets/materials/utils/surface_utils.py
+++ b/infinigen/assets/materials/utils/surface_utils.py
@@ -1,14 +1,16 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Mingzhe Wang
+# Authors: Mingzhe Wang, Lingjie Mei
import random
import math
+import numpy as np
+
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
-from infinigen.core.nodes import node_utils
+from infinigen.core.nodes import Nodes, node_utils
from infinigen.core.util.color import color_category
from infinigen.core import surface
@@ -19,24 +21,24 @@ def nodegroup_norm_value(nw: NodeWrangler):
group_input = nw.new_node(Nodes.GroupInput,
expose_input=[('NodeSocketFloat', 'Attribute', 0.0000),
('NodeSocketGeometry', 'Geometry', None)])
-
+
attribute_statistic_1 = nw.new_node(Nodes.AttributeStatistic,
input_kwargs={'Geometry': group_input.outputs["Geometry"], 2: group_input.outputs["Attribute"]})
-
+
subtract = nw.new_node(Nodes.Math,
input_kwargs={0: group_input.outputs["Attribute"], 1: attribute_statistic_1.outputs["Min"]},
attrs={'operation': 'SUBTRACT'})
-
+
subtract_1 = nw.new_node(Nodes.Math,
input_kwargs={0: attribute_statistic_1.outputs["Max"], 1: attribute_statistic_1.outputs["Min"]},
attrs={'operation': 'SUBTRACT'})
-
+
divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: subtract_1}, attrs={'operation': 'DIVIDE'})
-
+
subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'SUBTRACT'})
-
+
multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
-
+
group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Value': multiply}, attrs={'is_active_output': True})
@node_utils.to_nodegroup('nodegroup_norm_vec', singleton=False, type='GeometryNodeTree')
@@ -47,24 +49,24 @@ def nodegroup_norm_vec(nw: NodeWrangler):
expose_input=[('NodeSocketGeometry', 'Geometry', None),
('NodeSocketString', 'Name', ''),
('NodeSocketVector', 'Vector', (0.0000, 0.0000, 0.0000))])
-
+
separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Vector"]})
-
+
normvalue = nw.new_node(nodegroup_norm_value().name,
input_kwargs={'Attribute': separate_xyz_1.outputs["X"], 'Geometry': group_input.outputs["Geometry"]})
-
+
normvalue_1 = nw.new_node(nodegroup_norm_value().name,
input_kwargs={'Attribute': separate_xyz_1.outputs["Y"], 'Geometry': group_input.outputs["Geometry"]})
-
+
normvalue_2 = nw.new_node(nodegroup_norm_value().name,
input_kwargs={'Attribute': separate_xyz_1.outputs["Z"], 'Geometry': group_input.outputs["Geometry"]})
-
+
combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': normvalue, 'Y': normvalue_1, 'Z': normvalue_2})
-
+
store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
input_kwargs={'Geometry': group_input.outputs["Geometry"], 'Name': group_input.outputs["Name"], 2: combine_xyz},
attrs={'data_type': 'FLOAT_VECTOR'})
-
+
group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': store_named_attribute}, attrs={'is_active_output': True})
@@ -107,7 +109,7 @@ def sample_color(color, offset=0, keep_sum=False):
color[i] = mean-offset*(f*pcg+(1-f)*(1-pcg))
f = 0
return
-
+
for i in range(3):
if offset == 0:
color[i] = random.random()
@@ -180,3 +182,19 @@ def geo_voronoi_noise(nw, rand=False, **input_kwargs):
group_output = nw.new_node(Nodes.GroupOutput,
input_kwargs={'Geometry': capture_attribute.outputs["Geometry"],
'Attribute': capture_attribute.outputs["Attribute"]})
+
+
+def perturb_coordinates(nw, node, location, rotation):
+ for name in ['Generated', 'Object', "Position", "UV"]:
+ if name in node.outputs:
+ node_socket = node.outputs[name]
+ to_links = nw.find_to(node_socket)
+ if len(to_links) == 0:
+ continue
+ shifted = nw.new_node(Nodes.Mapping, [node_socket], input_kwargs={'Location': location,
+ 'Rotation': nw.combine(0, 0, rotation)}).outputs[0]
+ to_sockets = [tl.to_socket for tl in to_links]
+ for to_link in to_links:
+ nw.links.remove(to_link)
+ for to_socket in to_sockets:
+ nw.connect_input(shifted, to_socket)
diff --git a/infinigen/assets/materials/vase_shaders.py b/infinigen/assets/materials/vase_shaders.py
new file mode 100644
index 000000000..674f9a553
--- /dev/null
+++ b/infinigen/assets/materials/vase_shaders.py
@@ -0,0 +1,37 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category, hsv2rgba
+from infinigen.core import surface
+
+
+def shader_ceramic(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+ hsv = (uniform(0.0, 1.0), uniform(0.0, 0.75), uniform(0.0, 0.3))
+
+ rgb = nw.new_node(Nodes.RGB)
+ rgb.outputs[0].default_value = hsv2rgba(hsv)
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': rgb, 'Subsurface': 0.3, 'Subsurface Radius': (0.002, 0.002, 0.002), 'Subsurface Color': rgb, 'Subsurface IOR': 1.4700, 'Subsurface Anisotropy': 0.2000, 'Specular': 0.2000, 'Roughness': 0.0500, 'Clearcoat': 0.5000, 'Clearcoat Roughness': 0.0500, 'IOR': 1.4700})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf}, attrs={'is_active_output': True})
+
+def shader_glass(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ hsv = (uniform(0.0, 1.0), uniform(0.0, 0.2), 1.0)
+
+ glass_bsdf = nw.new_node(Nodes.GlassBSDF, input_kwargs={'Color': hsv2rgba(hsv), 'Roughness': uniform(0.05, 0.2)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True})
+
diff --git a/infinigen/assets/materials/water.py b/infinigen/assets/materials/water.py
index 7d90f3470..0fd2de9b9 100644
--- a/infinigen/assets/materials/water.py
+++ b/infinigen/assets/materials/water.py
@@ -177,6 +177,7 @@ def shader(
emissive_foam=False,
volume_density=("uniform", 0.07, 0.09),
anisotropy=("clip_gaussian", 0.75, 0.2, 0.5, 1),
+ mix_surface=False,
random_seed=0,
):
nw.force_input_consistency()
@@ -195,12 +196,14 @@ def shader(
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
"Base Color": color_of_transparent_bsdf_principled_bsdf, "Roughness": 0.0, "IOR": 1.33, "Transmission": 1.0
})
- surface_shader = nw.new_node(Nodes.MixShader, input_kwargs={
- 'Fac': nw.scalar_multiply(1.0, light_path.outputs["Is Camera Ray"]),
- 1: transparent_bsdf,
- 2: principled_bsdf
- })
-
+ if mix_surface:
+ surface_shader = nw.new_node(Nodes.MixShader, input_kwargs={
+ 'Fac': nw.scalar_multiply(1.0, light_path.outputs["Is Camera Ray"]),
+ 1: transparent_bsdf,
+ 2: principled_bsdf
+ })
+ else:
+ surface_shader = principled_bsdf
if asset_paths != []:
if emissive_foam:
foam_bsdf = nw.new_node(Nodes.Emission, input_kwargs={'Strength': 1})
diff --git a/infinigen/assets/materials/wear_tear/procedural_edge_wear.py b/infinigen/assets/materials/wear_tear/procedural_edge_wear.py
new file mode 100644
index 000000000..1b4c44854
--- /dev/null
+++ b/infinigen/assets/materials/wear_tear/procedural_edge_wear.py
@@ -0,0 +1,276 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+# Acknowledgement: This file draws inspiration from following sources:
+
+# https://www.youtube.com/watch?v=Aa8gf1pwb4E by Riley Brown
+# https://www.youtube.com/watch?v=EQ149bMtKRA by Christopher Fraser
+# https://www.youtube.com/watch?v=lDbsHpqKgoI by The DiNusty Empire
+# https://www.youtube.com/watch?v=bLRwf2rZiAs by DECODED
+# https://www.youtube.com/watch?v=NnlaIizA_AQ by Aryan
+# https://www.youtube.com/watch?v=_wEXl3LncAc by diivja
+
+
+from numpy.random import uniform, choice
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+import logging
+from collections.abc import Iterable
+
+logger = logging.getLogger(__name__)
+
+def get_edge_wear_params():
+ return {
+ '_worn_off_opacity': uniform(0.01),
+ '_worn_off_radius': uniform(0.005, 0.01),
+ '_scratch_radius': uniform(0.01, 0.03),
+ '_worn_off_mask_randomness': uniform(2.5, 3.0),
+ '_edge_base_color_hue': uniform(0.0, 1.0),
+ '_edge_base_color_whiteness': uniform(0.1, 0.6),
+ '_scratch_mask_randomness': choice([uniform(0.1, 5.0), uniform(1., 10.0)]),
+ '_scratch_density': choice([uniform(1.5, 10.0)]),
+ '_scratch_opacity': uniform(0.5, 1.0),
+ }
+
+
+def shader_edge_tear_free_node_group(nw: NodeWrangler,
+ original_bsdf,
+ original_displacement,
+ _worn_off_opacity=0.5,
+ _worn_off_radius=0.015,
+ _scratch_radius=0.01,
+ _worn_off_mask_randomness=2.0,
+ _edge_base_color_hue=1.0,
+ _edge_base_color_whiteness=0.1,
+ _scratch_mask_randomness=0.5,
+ _scratch_density=5.0,
+ _scratch_opacity=0.2,
+ ):
+
+ scratch_opacity = nw.new_node(Nodes.Value)
+ scratch_opacity.outputs[0].default_value = _scratch_opacity
+
+ scratch_mask_randomness = nw.new_node(Nodes.Value)
+ scratch_mask_randomness.outputs[0].default_value = _scratch_mask_randomness
+
+ scratch_radius = nw.new_node(Nodes.Value)
+ scratch_radius.outputs[0].default_value = _scratch_radius
+
+ worn_off_opacity = nw.new_node(Nodes.Value)
+ worn_off_opacity.outputs[0].default_value = _worn_off_opacity
+
+ paint_worn_off_radius = nw.new_node(Nodes.Value)
+ paint_worn_off_radius.outputs[0].default_value = _worn_off_radius
+
+ worn_off_mask_randomness = nw.new_node(Nodes.Value)
+ worn_off_mask_randomness.outputs[0].default_value = _worn_off_mask_randomness
+
+ edge_base_color_whiteness = nw.new_node(Nodes.Value)
+ edge_base_color_whiteness.outputs[0].default_value = _edge_base_color_whiteness
+
+ edge_base_color_hue = nw.new_node(Nodes.Value)
+ edge_base_color_hue.outputs[0].default_value = _edge_base_color_hue
+
+ scratch_density = nw.new_node(Nodes.Value)
+ scratch_density.outputs[0].default_value = _scratch_density
+
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': texture_coordinate.outputs["Object"]})
+
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': scratch_mask_randomness, 'Detail': 1.0000})
+
+ color_ramp = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture.outputs["Fac"]})
+ color_ramp.color_ramp.elements[0].position = 0.4436
+ color_ramp.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp.color_ramp.elements[1].position = 0.5345
+ color_ramp.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+
+ bevel = nw.new_node('ShaderNodeBevel', input_kwargs={'Radius': scratch_radius}, attrs={'samples': 20})
+
+ geometry = nw.new_node(Nodes.NewGeometry)
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bevel, 1: geometry.outputs["Normal"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ absolute = nw.new_node(Nodes.Math, input_kwargs={0: subtract.outputs["Vector"]}, attrs={'operation': 'ABSOLUTE'})
+
+ color_ramp_1 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': absolute})
+ color_ramp_1.color_ramp.elements[0].position = 0.0691
+ color_ramp_1.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp_1.color_ramp.elements[1].position = 0.1564
+ color_ramp_1.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: color_ramp.outputs["Color"], 1: color_ramp_1.outputs["Color"]},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: scratch_opacity, 1: multiply},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+
+ bevel_1 = nw.new_node('ShaderNodeBevel', input_kwargs={'Radius': paint_worn_off_radius}, attrs={'samples': 20})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: bevel_1, 1: geometry.outputs["Normal"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ absolute_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1.outputs["Vector"]}, attrs={'operation': 'ABSOLUTE'})
+
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'Scale': worn_off_mask_randomness, 'Detail': 1.0000})
+
+ color_ramp_2 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Fac"]})
+ color_ramp_2.color_ramp.elements[0].position = 0.0764
+ color_ramp_2.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp_2.color_ramp.elements[1].position = 0.5709
+ color_ramp_2.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: absolute_1, 1: color_ramp_2.outputs["Color"]},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: worn_off_opacity, 1: multiply_2},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ color_ramp_3 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': multiply_3})
+ color_ramp_3.color_ramp.elements[0].position = 0.0000
+ color_ramp_3.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp_3.color_ramp.elements[1].position = 0.7782
+ color_ramp_3.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ combine_color = nw.new_node(Nodes.CombineColor,
+ input_kwargs={'Red': edge_base_color_hue, 'Green': 0.7733, 'Blue': 0.0100},
+ attrs={'mode': 'HSV'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={0: edge_base_color_whiteness, 6: combine_color, 7: (0.02, 0.02, 0.02, 1.0000)},
+ attrs={'clamp_result': True, 'data_type': 'RGBA', 'clamp_factor': False})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': mix.outputs[2]})
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': reroute, 'Metallic': 0.3745, 'Specular': 0.0000, 'Roughness': 0.1436})
+
+ mix_shader = nw.new_node(Nodes.MixShader,
+ input_kwargs={'Fac': color_ramp_3.outputs["Color"], 1: original_bsdf, 2: principled_bsdf})
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (10.0000, 1.0000, 1.0000)})
+
+
+ voronoi_texture = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mapping_1, 'Scale': scratch_density},
+ attrs={'feature': 'DISTANCE_TO_EDGE'})
+
+ mapping_2 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (1.0000, 10.0000, 1.0000)})
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: scratch_density, 'Scale': 2.0000}, attrs={'operation': 'SCALE'})
+
+ voronoi_texture_1 = nw.new_node(Nodes.VoronoiTexture,
+ input_kwargs={'Vector': mapping_2, 'Scale': scale.outputs["Vector"]},
+ attrs={'feature': 'DISTANCE_TO_EDGE'})
+
+ multiply_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: voronoi_texture.outputs["Distance"], 1: voronoi_texture_1.outputs["Distance"]},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ color_ramp_6 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': multiply_4})
+ color_ramp_6.color_ramp.elements[0].position = 0.0000
+ color_ramp_6.color_ramp.elements[0].color = [1.0000, 1.0000, 1.0000, 1.0000]
+ color_ramp_6.color_ramp.elements[1].position = 0.0073
+ color_ramp_6.color_ramp.elements[1].color = [0.0000, 0.0000, 0.0000, 1.0000]
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: color_ramp_1.outputs["Color"], 1: color_ramp_6.outputs["Color"]},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ multiply_6 = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: multiply_5},
+ attrs={'use_clamp': True, 'operation': 'MULTIPLY'})
+
+ principled_bsdf_1 = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': reroute, 'Metallic': 0.3855, 'Specular': 0.0000, 'Roughness': 0.0000})
+
+ mix_shader_1 = nw.new_node(Nodes.MixShader, input_kwargs={'Fac': multiply_1, 1: mix_shader, 2: principled_bsdf_1})
+
+ # add operation
+ scale_multiply6 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: multiply_6, 'Scale': 2.0000},
+ attrs={'operation': 'SCALE'})
+
+ if original_displacement is None:
+ total_displacement = scale_multiply6
+ else:
+ total_displacement = nw.new_node(Nodes.Math,
+ input_kwargs={0: original_displacement, 1: scale_multiply6},
+ attrs={'operation': 'ADD', 'use_clamp': True})
+
+ return mix_shader_1, total_displacement
+
+
+MARKER_LABEL = "wear_tear"
+
+def apply_over(obj, selection=None, **shader_kwargs):
+
+ # get all materials
+ # https://blenderartists.org/t/finding-out-if-an-object-has-a-material/512570/6
+ materials = obj.data.materials.items()
+ if len(materials) == 0:
+ logging.warning(f"No material exist for {obj.name}! Scratches can only be applied over some existing material.")
+ return
+
+ if len(shader_kwargs) == 0:
+ logging.debug("Obtaining Randomized Scratch Parameters")
+ shader_kwargs = get_edge_wear_params()
+
+ for material_name, material in materials:
+
+ # get material node tree
+ # https://blender.stackexchange.com/questions/240278/how-to-access-shader-node-via-python-script
+ material_node_tree = material.node_tree
+
+ if any([node.label == MARKER_LABEL for node in material_node_tree.nodes]):
+ continue
+
+ nw = NodeWrangler(material_node_tree)
+
+ result = nw.find("ShaderNodeOutputMaterial")
+ if len(result) == 0:
+ logger.warning("No Material Output Node found in the object's materials! Returning")
+ continue
+
+ # get nodes and links connected to specific inputs
+ # https://blender.stackexchange.com/questions/5462/is-it-possible-to-find-the-nodes-connected-to-a-node-in-python
+ initial_bsdf = result[0].inputs['Surface'].links[0].from_node
+ displacement_links = result[0].inputs['Displacement'].links
+
+ if len(displacement_links) == 0:
+ initial_displacement = None
+ else:
+ initial_displacement = result[0].inputs['Displacement'].links[0].from_node
+
+ final_bsdf, final_displacement = shader_edge_tear_free_node_group(nw, initial_bsdf, initial_displacement, **shader_kwargs)
+ # connecting nodes
+ # https://blender.stackexchange.com/questions/101820/how-to-add-remove-links-to-existing-or-new-nodes-using-python
+ material_node_tree.links.new(final_bsdf.outputs[0], result[0].inputs['Surface'])
+ material_node_tree.links.new(final_displacement.outputs[0], result[0].inputs['Displacement'])
+
+ final_bsdf.label = MARKER_LABEL
+
+ return
+
+def apply(obj):
+ if not isinstance(obj, Iterable):
+ obj = [obj]
+ for o in obj:
+ apply_over(o)
\ No newline at end of file
diff --git a/infinigen/assets/materials/wear_tear/procedural_scratch.py b/infinigen/assets/materials/wear_tear/procedural_scratch.py
new file mode 100644
index 000000000..2a3ebc5fc
--- /dev/null
+++ b/infinigen/assets/materials/wear_tear/procedural_scratch.py
@@ -0,0 +1,170 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Meenal Parakh
+# Acknowledgement: This file draws inspiration from following sources:
+
+# https://www.youtube.com/watch?v=l0whu3494_c by Ryan King Art
+# https://www.youtube.com/watch?v=qMCuDjXjsZ0 by Ryan King Art
+# https://www.youtube.com/watch?v=L3SvNpjIERs by Sina Sinaie
+# https://www.youtube.com/watch?v=0B-lexp10jk by Holmes Motion
+# https://www.youtube.com/watch?v=ewq69iNRdmQ by Ryan King Art
+# https://www.youtube.com/watch?v=MH8iutCKtYc by ChuckCG
+
+
+from numpy.random import uniform, choice
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+import logging
+from collections.abc import Iterable
+
+from infinigen.core.util.random import log_uniform
+
+logger = logging.getLogger(__name__)
+
+
+def get_scratch_params():
+ return {
+ 'angle1': uniform(10., 80.0),
+ 'angle2': uniform(-80.0, -10.0),
+ 'scratch_scale': log_uniform(5,80),
+ 'scratch_mask_ratio': log_uniform(0.01, 0.9),
+ 'scratch_mask_noise': log_uniform(5, 40),
+ 'scratch_depth': log_uniform(.1,1.),
+ }
+
+def scratch_shader(nw: NodeWrangler,
+ original_bsdf,
+ angle1=45.0,
+ angle2=-20.0,
+ scratch_scale=20.0,
+ scratch_mask_ratio=0.8,
+ scratch_mask_noise=10.0,
+ scratch_depth=0.1):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
+
+ n_angle1 = nw.new_node(Nodes.Value)
+ n_angle1.outputs[0].default_value = angle1
+
+ n_angle2 = nw.new_node(Nodes.Value)
+ n_angle2.outputs[0].default_value = angle2
+
+ n_scratch_scale = nw.new_node(Nodes.Value)
+ n_scratch_scale.outputs[0].default_value = scratch_scale
+
+ n_scratch_mask_ratio = nw.new_node(Nodes.Value)
+ n_scratch_mask_ratio.outputs[0].default_value = scratch_mask_ratio
+
+ n_scratch_mask_noise = nw.new_node(Nodes.Value)
+ n_scratch_mask_noise.outputs[0].default_value = scratch_mask_noise
+
+ n_scratch_depth = nw.new_node(Nodes.Value)
+ n_scratch_depth.outputs[0].default_value = scratch_depth
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': n_angle1})
+
+ mapping_1 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'Rotation': combine_xyz, 'Scale': (25.0000, 1.0000, 1.0000)},
+ attrs={'vector_type': 'TEXTURE'})
+
+ noise_texture_3 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'Scale': n_scratch_scale, 'Detail': 15.0000, 'Roughness': 0.0000, 'Distortion': 22.8000})
+
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': n_angle2})
+
+ mapping_2 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'Rotation': combine_xyz_1, 'Scale': (25.0000, 1.0000, 1.0000)},
+ attrs={'vector_type': 'TEXTURE'})
+
+ noise_texture_5 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_2, 'Scale': n_scratch_scale, 'Detail': 15.0000, 'Roughness': 0.0000, 'Distortion': 22.8000})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: noise_texture_3.outputs["Fac"], 1: noise_texture_5.outputs["Fac"]})
+
+ mapping_3 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate_1.outputs["Object"], 'Rotation': (0.1588, -0.5742, 0.1920)},
+ attrs={'vector_type': 'TEXTURE'})
+
+ noise_texture_6 = nw.new_node(Nodes.NoiseTexture, input_kwargs={'Vector': mapping_3, 'Scale': n_scratch_mask_noise, 'Detail': 1.0000})
+
+ color_ramp_2 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_6.outputs["Fac"]})
+ color_ramp_2.color_ramp.elements[0].position = 0.4109
+ color_ramp_2.color_ramp.elements[0].color = [0.0000, 0.0000, 0.0000, 1.0000]
+ color_ramp_2.color_ramp.elements[1].position = 1.0000
+ color_ramp_2.color_ramp.elements[1].color = [1.0000, 1.0000, 1.0000, 1.0000]
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: n_scratch_mask_ratio, 1: color_ramp_2.outputs["Color"]}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: multiply}, attrs={'use_clamp': True})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': add_1, 1: 0.7000, 2: 0.7200, 4: 0.9000})
+ # scaled_scratch = nw.new_node(Nodes.Math, input_kwargs={0: n_scratch_depth, 1: map_range.outputs["Result"]}, attrs={'operation': 'MULTIPLY'})
+
+ # material_output = nw.new_node(Nodes.MaterialOutput,
+ # input_kwargs={'Surface': original_bsdf, 'Displacement': scaled_scratch},
+ # attrs={'is_active_output': True})
+
+ # return material_output
+
+ bump = nw.new_node(Nodes.Bump, input_kwargs={'Strength': n_scratch_depth, 'Height': map_range.outputs["Result"]})
+ return {'Normal': bump}
+
+
+def find_normal_input(bsdf):
+ for i, o in enumerate(bsdf.inputs):
+ if o.name == "Normal":
+ return i
+ logger.debug(f"Normal not found for {bsdf}")
+ return None
+
+MARKER_LABEL = "scratch"
+
+def apply_over(obj, selection=None, **shader_kwargs):
+
+ # get all materials
+ # https://blenderartists.org/t/finding-out-if-an-object-has-a-material/512570/6
+ materials = obj.data.materials.items()
+ if len(materials) == 0:
+ logging.warning(f"No material exist for {obj.name}! Scratches can only be applied over some existing material.")
+ return
+
+ if len(shader_kwargs) == 0:
+ logging.debug("Obtaining Randomized Scratch Parameters")
+ shader_kwargs = get_scratch_params()
+
+ for material_name, material in materials:
+
+ # get material node tree
+ # https://blender.stackexchange.com/questions/240278/how-to-access-shader-node-via-python-script
+ material_node_tree = material.node_tree
+
+ if any([n.label == MARKER_LABEL for n in material_node_tree.nodes]):
+ logging.warning(f"Scratch already applied to {material_name}! Skipping")
+ continue
+
+ nw = NodeWrangler(material_node_tree)
+
+ result = nw.find_recursive("ShaderNodeBsdf")
+ if len(result) == 0:
+ logging.debug("No BSDF found in the object's materials! Returning")
+ continue
+
+ nw_bsdf, bsdf = result[-1]
+ # final_bsdf = scratch_shader(nw_bsdf, bsdf, **shader_kwargs)
+
+ if 'Normal' in bsdf.inputs.keys():
+ if len(nw_bsdf.find_from(bsdf.inputs['Normal'])) == 0:
+ bump = scratch_shader(nw_bsdf, None, **shader_kwargs)['Normal']
+
+ # connecting nodes: https://blender.stackexchange.com/questions/101820/how-to-add-remove-links-to-existing-or-new-nodes-using-python
+ nw_bsdf.links.new(bump.outputs[0], bsdf.inputs['Normal'])
+
+ nw_bsdf.label = MARKER_LABEL
+
+def apply(obj):
+ if not isinstance(obj, Iterable):
+ obj = [obj]
+ for o in obj:
+ apply_over(o)
\ No newline at end of file
diff --git a/infinigen/assets/materials/wood.py b/infinigen/assets/materials/wood.py
deleted file mode 100644
index 235f62942..000000000
--- a/infinigen/assets/materials/wood.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-
-# Authors: Mingzhe Wang
-
-
-import os, sys
-import numpy as np
-import math as ma
-from infinigen.assets.materials.utils.surface_utils import clip, sample_range, sample_ratio, sample_color, geo_voronoi_noise
-import bpy
-import mathutils
-from numpy.random import uniform, normal, randint
-from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
-from infinigen.core.nodes import node_utils
-from infinigen.core.util.color import color_category
-from infinigen.core import surface
-
-def shader_wood(nw: NodeWrangler, rand=False, **input_kwargs):
- # Code generated using version 2.4.3 of the node_transpiler
-
- texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
-
- mapping_2 = nw.new_node(Nodes.Mapping,
- input_kwargs={'Vector': texture_coordinate_1.outputs["Generated"], 'Rotation': uniform(0,ma.pi*2, 3)})
-
- mapping_1 = nw.new_node(Nodes.Mapping,
- input_kwargs={'Vector': mapping_2, 'Scale': (0.5, sample_range(2, 4) if rand else 3, 0.5)})
-
- musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture,
- input_kwargs={'Vector': mapping_1, 'Scale': 2.0},
- attrs={'musgrave_dimensions': '4D'})
- if rand:
- musgrave_texture_2.inputs['W'].default_value = sample_range(0, 5)
- musgrave_texture_2.inputs['Scale'].default_value = sample_ratio(2.0, 3/4, 4/3)
-
- noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
- input_kwargs={'Vector': musgrave_texture_2, 'W': 0.7, 'Scale': 10.0},
- attrs={'noise_dimensions': '4D'})
- if rand:
- noise_texture_1.inputs['W'].default_value = sample_range(0, 5)
- noise_texture_1.inputs['Scale'].default_value = sample_ratio(5, 0.5, 2)
-
- colorramp_2 = nw.new_node(Nodes.ColorRamp,
- input_kwargs={'Fac': noise_texture_1.outputs["Fac"]})
- colorramp_2.color_ramp.elements.new(0)
- colorramp_2.color_ramp.elements[0].position = 0.1727
- colorramp_2.color_ramp.elements[0].color = (0.1567, 0.0162, 0.0017, 1.0)
- colorramp_2.color_ramp.elements[1].position = 0.4364
- colorramp_2.color_ramp.elements[1].color = (0.2908, 0.1007, 0.0148, 1.0)
- colorramp_2.color_ramp.elements[2].position = 0.5864
- colorramp_2.color_ramp.elements[2].color = (0.0814, 0.0344, 0.0125, 1.0)
- if rand:
- colorramp_2.color_ramp.elements[0].position += sample_range(-0.05, 0.05)
- colorramp_2.color_ramp.elements[1].position += sample_range(-0.1, 0.1)
- colorramp_2.color_ramp.elements[2].position += sample_range(-0.05, 0.05)
- for e in colorramp_2.color_ramp.elements:
- sample_color(e.color, offset=0.03)
-
- colorramp_4 = nw.new_node(Nodes.ColorRamp,
- input_kwargs={'Fac': noise_texture_1.outputs["Fac"]})
- colorramp_4.color_ramp.elements[0].position = 0.0
- colorramp_4.color_ramp.elements[0].color = (0.4855, 0.4855, 0.4855, 1.0)
- colorramp_4.color_ramp.elements[1].position = 1.0
- colorramp_4.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
-
- principled_bsdf_1 = nw.new_node(Nodes.PrincipledBSDF,
- input_kwargs={'Base Color': colorramp_2.outputs["Color"], 'Roughness': colorramp_4.outputs["Color"]},
- attrs={'subsurface_method': 'BURLEY'})
-
- material_output = nw.new_node(Nodes.MaterialOutput,
- input_kwargs={'Surface': principled_bsdf_1})
-
-def apply(obj, geo_kwargs=None, shader_kwargs=None, **kwargs):
- surface.add_material(obj, shader_wood, reuse=False, input_kwargs=shader_kwargs)
-
-
-if __name__ == "__main__":
- mat = 'wood'
- if not os.path.isdir(os.path.join('outputs', mat)):
- os.mkdir(os.path.join('outputs', mat))
- for i in range(10):
- bpy.ops.wm.open_mainfile(filepath='test.blend')
- apply(bpy.data.objects['SolidModel'], geo_kwargs={'rand':True}, shader_kwargs={'rand': True})
- #fn = os.path.join(os.path.abspath(os.curdir), 'giraffe_geo_test.blend')
- #bpy.ops.wm.save_as_mainfile(filepath=fn)
- bpy.context.scene.render.filepath = os.path.join('outputs', mat, '%s_%d.jpg'%(mat, i))
- bpy.context.scene.render.image_settings.file_format='JPEG'
- bpy.ops.render.render(write_still=True)
\ No newline at end of file
diff --git a/infinigen/assets/materials/woods/composite_wood_tile.py b/infinigen/assets/materials/woods/composite_wood_tile.py
new file mode 100644
index 000000000..218bc27e9
--- /dev/null
+++ b/infinigen/assets/materials/woods/composite_wood_tile.py
@@ -0,0 +1,18 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from functools import wraps
+
+from numpy.random import uniform
+
+from infinigen.core.nodes import Nodes
+from infinigen.core.util.random import log_uniform
+from .tiled_wood import shader_wood_tiled
+from .. import shader_wood, tile
+from ..tile import shader_staggered_tile
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, shape=None, **kwargs):
+ shader_func = shader_wood
+ tile.apply(obj, selection, vertical, shader_func, scale, alternating, 'composite', **kwargs)
diff --git a/infinigen/assets/materials/woods/crossed_wood_tile.py b/infinigen/assets/materials/woods/crossed_wood_tile.py
new file mode 100644
index 000000000..685915b2a
--- /dev/null
+++ b/infinigen/assets/materials/woods/crossed_wood_tile.py
@@ -0,0 +1,14 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .. import tile
+from .wood import shader_wood
+from ...utils.object import new_plane
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, shape=None, **kwargs):
+ shader_func = shader_wood
+ tile.apply(obj, selection, vertical, shader_func, scale, False, 'crossed', **kwargs)
+
+# def make_sphere():e
diff --git a/infinigen/assets/materials/woods/hexagon_wood_tile.py b/infinigen/assets/materials/woods/hexagon_wood_tile.py
new file mode 100644
index 000000000..e530c0c46
--- /dev/null
+++ b/infinigen/assets/materials/woods/hexagon_wood_tile.py
@@ -0,0 +1,12 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .. import tile
+from .wood import shader_wood
+from ...utils.object import new_plane
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, shape=None, **kwargs):
+ shader_func = shader_wood
+ tile.apply(obj, selection, vertical, shader_func, scale, alternating, 'hexagon', **kwargs)
diff --git a/infinigen/assets/materials/woods/non_wood_tile.py b/infinigen/assets/materials/woods/non_wood_tile.py
new file mode 100644
index 000000000..d63aa058e
--- /dev/null
+++ b/infinigen/assets/materials/woods/non_wood_tile.py
@@ -0,0 +1,22 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+
+
+def apply(
+ obj, selection=None, vertical=False, shader_func=None, scale=None, alternating=None, shape=None,
+ **kwargs
+):
+ from .. import tile
+ from .wood import shader_wood
+ shader_funcs = tile.get_shader_funcs()
+ shader_funcs = [(f, w) for f, w in shader_funcs if f != shader_wood]
+ funcs, weights = zip(*shader_funcs)
+ weights = np.array(weights) / sum(weights)
+ if shader_func is None:
+ shader_func = np.random.choice(funcs, p=weights)
+ if shape is None:
+ shape = np.random.choice(['square', 'hexagon', 'rectangle'])
+ tile.apply(obj, selection, vertical, shader_func, scale, alternating, shape, **kwargs)
diff --git a/infinigen/assets/materials/woods/square_wood_tile.py b/infinigen/assets/materials/woods/square_wood_tile.py
new file mode 100644
index 000000000..d86306616
--- /dev/null
+++ b/infinigen/assets/materials/woods/square_wood_tile.py
@@ -0,0 +1,14 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .. import tile
+from .wood import shader_wood
+from ...utils.object import new_plane
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, shape=None, **kwargs):
+ shader_func = shader_wood
+ tile.apply(obj, selection, vertical, shader_func, scale, alternating, 'square', **kwargs)
+
+# def make_sphere():e
diff --git a/infinigen/assets/materials/woods/staggered_wood_tile.py b/infinigen/assets/materials/woods/staggered_wood_tile.py
new file mode 100644
index 000000000..e28507145
--- /dev/null
+++ b/infinigen/assets/materials/woods/staggered_wood_tile.py
@@ -0,0 +1,12 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .. import tile
+from .wood import shader_wood
+from ...utils.object import new_plane
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, shape=None, **kwargs):
+ shader_func = shader_wood
+ tile.apply(obj, selection, vertical, shader_func, scale, alternating, 'staggered', **kwargs)
diff --git a/infinigen/assets/materials/woods/tiled_wood.py b/infinigen/assets/materials/woods/tiled_wood.py
new file mode 100644
index 000000000..3cc818923
--- /dev/null
+++ b/infinigen/assets/materials/woods/tiled_wood.py
@@ -0,0 +1,188 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=rd2jhGV6tqo by Ryan King Art
+
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint, choice
+
+from infinigen.assets.materials import common
+from infinigen.assets.materials.woods.wood import get_color
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import rgb2hsv
+
+from infinigen.core.util.random import clip_gaussian
+from infinigen.assets.materials.bark_random import get_random_bark_params, hex_to_rgb
+
+
+
+@node_utils.to_nodegroup('nodegroup_tiling', singleton=False, type='ShaderNodeTree')
+def nodegroup_tiling(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Horizontal Scale', 0.5000),
+ ('NodeSocketFloat', 'Vertical Scale', 0.5),
+ ('NodeSocketFloat', 'Seed', 0.5000)])
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: 1.0000, 1: group_input.outputs["Horizontal Scale"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply})
+
+ vec = texture_coordinate.outputs["Object"]
+
+ vec = nw.new_node(Nodes.Mapping, [vec, uniform(0, 1, 3)])
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: vec, 1: combine_xyz})
+
+ divide_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: 1.0000, 1: group_input.outputs["Vertical Scale"]},
+ attrs={'operation': 'DIVIDE'})
+
+ brick_texture = nw.new_node(Nodes.BrickTexture,
+ input_kwargs={'Vector': add.outputs["Vector"], 'Color2': (0,0,0, 1.0000), 'Scale': 1.0000, 'Mortar Size': 0.0050, 'Mortar Smooth': 1.0000, 'Bias': -0.5000, 'Brick Width': divide_1, 'Row Height': divide},
+ attrs={'squash_frequency': 1})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: brick_texture.outputs["Color"], 1: 1000.0000, 2: group_input.outputs["Seed"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Tile Color': brick_texture.outputs["Color"], 'Seed': multiply_add, 'Displacement': brick_texture.outputs["Color"]},
+ attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_tiled_wood', singleton=False, type='ShaderNodeTree')
+def nodegroup_tiled_wood(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ texture_coordinate = nw.new_node(Nodes.TextureCoord)
+
+ mapping_2 = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (5.0000, 100.0000, 100.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Tile Horizontal Scale', 0.0000),
+ ('NodeSocketFloat', 'Tile Vertical Scale', 2.9600),
+ ('NodeSocketColor', 'Main Color', (0.0000, 0.0000, 0.0000, 1.0000)),
+ ('NodeSocketFloat', 'Seed', 0.0000)])
+
+ group = nw.new_node(nodegroup_tiling().name,
+ input_kwargs={'Horizontal Scale': group_input.outputs["Tile Horizontal Scale"], 'Vertical Scale': group_input.outputs["Tile Vertical Scale"], 'Seed': group_input.outputs["Seed"]})
+
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': mapping_2, 'W': group.outputs["Seed"], 'Scale': 10.0000, 'Detail': 15.0000, 'Dimension': 7.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_2, 3: 1.0000, 4: -1.0000})
+
+ mapping_1 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': texture_coordinate.outputs["Object"]})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping_1, 'W': group.outputs["Seed"], 'Scale': 0.5000, 'Detail': 1.0000, 'Distortion': 1.1000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'W': group.outputs["Seed"], 'Scale': noise_texture_1.outputs["Fac"], 'Detail': 15.0000, 'Dimension': 0.2000, 'Lacunarity': 2.4000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_1, 3: -1.4000, 4: 1.5000})
+
+ map_range_1 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': map_range.outputs["Result"], 3: 1.0000, 4: 0.5000})
+
+ mapping = nw.new_node(Nodes.Mapping,
+ input_kwargs={'Vector': texture_coordinate.outputs["Object"], 'Scale': (0.1500, 1.0000, 0.1500)})
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': group.outputs["Seed"], 'Detail': 5.0000, 'Distortion': 1.0000},
+ attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture,
+ input_kwargs={'Vector': noise_texture.outputs["Fac"], 'W': group.outputs["Seed"], 'Scale': 4.0000, 'Detail': 10.0000, 'Dimension': 0.0000},
+ attrs={'musgrave_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={6: noise_texture.outputs["Fac"], 7: musgrave_texture},
+ attrs={'data_type': 'RGBA'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: map_range_1.outputs["Result"], 7: mix.outputs[2]},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9500, 6: map_range_2.outputs["Result"], 7: mix_1.outputs[2]},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'})
+
+ hue_saturation_value = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Saturation': 0.8000, 'Value': 0.2000, 'Fac': 0.0, 'Color': group_input.outputs["Main Color"]})
+
+ mix_3 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: mix_2.outputs[2], 6: hue_saturation_value, 7: group_input.outputs["Main Color"]},
+ attrs={'data_type': 'RGBA'})
+
+ mix_4 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 1.0000, 6: mix_3.outputs[2], 7: group.outputs["Tile Color"]},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'})
+
+ color = mix_4.outputs[2]
+ roughness = nw.build_float_curve(color, [(0, uniform(.3, .5)), (1, uniform(.8, 1.))])
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={'Base Color': color, 'Roughness': roughness})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: mix_2.outputs[2], 1: 0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group.outputs["Displacement"], 1: multiply})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 0.0100}, attrs={'operation': 'MULTIPLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'BSDF': principled_bsdf, 'Displacement': multiply_1},
+ attrs={'is_active_output': True})
+
+
+def shader_wood_tiled(nw: NodeWrangler, hscale=None, vscale=None, base_color=None, seed=None, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ if hscale is None:
+ hscale = clip_gaussian(6, 4, 3, 9)
+ if vscale is None:
+ vscale = uniform(0.05, 0.2) * hscale
+ if seed is None:
+ seed = uniform(-1000.0, 1000.0)
+ if base_color is None:
+ base_color = get_color()
+
+ group = nw.new_node(nodegroup_tiled_wood().name,
+ input_kwargs={'Tile Horizontal Scale': hscale,
+ 'Tile Vertical Scale': vscale,
+ 'Seed': seed,
+ 'Main Color': base_color})
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': group.outputs["Displacement"], 'Midlevel': 0.0000})
+
+ material_output = nw.new_node(Nodes.MaterialOutput,
+ input_kwargs={'Surface': group.outputs["BSDF"], 'Displacement': displacement},
+ attrs={'is_active_output': True})
+
+
+def get_random_light_wood_params():
+ color_fac = [0xdeb887, 0xcdaa7d, 0xfff8dc]
+ color_factory = [hex_to_rgb(c) for c in color_fac]
+ return color_factory[randint(len(color_fac))]
+
+
+def apply(obj, selection=None, **kwargs):
+ common.apply(obj, shader_wood_tiled, selection=selection, **kwargs)
+
+# def make_sphere():
+# return new_plane()
diff --git a/infinigen/assets/materials/woods/wood.py b/infinigen/assets/materials/woods/wood.py
new file mode 100644
index 000000000..1891ab51d
--- /dev/null
+++ b/infinigen/assets/materials/woods/wood.py
@@ -0,0 +1,146 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=jDEijCwz6to by Lachlan Sarv
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import common
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.util.color import hsv2rgba, rgb2hsv
+from infinigen.core.util.random import log_uniform
+from ...utils.object import new_cube
+
+
+def get_color():
+ from infinigen.assets.materials.bark_random import get_random_bark_params
+ _, color_params = get_random_bark_params(np.random.randint(1e7))
+ h, s, v = rgb2hsv(color_params['Color'][:-1])
+ return hsv2rgba(h + uniform(-.0, .05), s + uniform(-.3, .2), v * log_uniform(.2, 20))
+
+
+def shader_wood(nw: NodeWrangler, color=None, w=None, vertical=False, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ vec = nw.new_node(Nodes.TextureCoord).outputs['Object']
+ if vertical:
+ vec = nw.new_node(
+ Nodes.Mapping, [vec], input_kwargs={'Rotation': (np.pi / 2, 0, np.pi / 2 * np.random.randint(2))}
+ )
+
+ mapping_2 = nw.new_node(
+ Nodes.Mapping, input_kwargs={
+ 'Vector': vec,
+ 'Scale': (5.0000, 100.0000, 100.0000)
+ })
+
+ if color is None:
+ color = get_color()
+ if w is None:
+ w = uniform(0, 1)
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture, input_kwargs={
+ 'Vector': mapping_2,
+ 'W': w,
+ 'Scale': 10.0000,
+ 'Detail': 15.0000,
+ 'Dimension': 7.0000
+ }, attrs={'musgrave_dimensions': '4D'})
+
+ map_range_2 = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_2, 3: 1.0000, 4: -1.0000})
+
+ mapping_1 = nw.new_node(Nodes.Mapping, input_kwargs={'Vector': vec})
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture, input_kwargs={
+ 'Vector': mapping_1,
+ 'W': w,
+ 'Scale': 0.5000,
+ 'Detail': 1.0000,
+ 'Distortion': 1.1000
+ }, attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture_1 = nw.new_node(Nodes.MusgraveTexture, input_kwargs={
+ 'W': w,
+ 'Scale': noise_texture_1.outputs["Fac"],
+ 'Detail': 15.0000,
+ 'Dimension': 0.2000,
+ 'Lacunarity': 2.4000
+ }, attrs={'musgrave_dimensions': '4D'})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': musgrave_texture_1, 3: -1.4000, 4: 1.5000})
+
+ map_range_1 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': map_range.outputs["Result"], 3: 1.0000, 4: 0.5000})
+
+ mapping = nw.new_node(Nodes.Mapping, input_kwargs={
+ 'Vector': vec,
+ 'Scale': (0.1500, 1.0000, 0.1500)
+ })
+
+ noise_texture = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': mapping, 'W': w, 'Detail': 5.0000, 'Distortion': 1.0000
+ }, attrs={'noise_dimensions': '4D'})
+
+ musgrave_texture = nw.new_node(Nodes.MusgraveTexture, input_kwargs={
+ 'Vector': noise_texture.outputs["Fac"],
+ 'W': w,
+ 'Scale': 4.0000,
+ 'Detail': 10.0000,
+ 'Dimension': 0.0000
+ }, attrs={'musgrave_dimensions': '4D'})
+
+ mix = nw.new_node(Nodes.Mix, input_kwargs={6: noise_texture.outputs["Fac"], 7: musgrave_texture},
+ attrs={'data_type': 'RGBA'})
+
+ mix_1 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9000, 6: map_range_1.outputs["Result"], 7: mix.outputs[2]},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'})
+
+ mix_2 = nw.new_node(Nodes.Mix,
+ input_kwargs={0: 0.9500, 6: map_range_2.outputs["Result"], 7: mix_1.outputs[2]},
+ attrs={'blend_type': 'MULTIPLY', 'data_type': 'RGBA'})
+
+ hue_saturation_value = nw.new_node('ShaderNodeHueSaturation',
+ input_kwargs={'Saturation': 0.8000, 'Value': 0.2000, 'Color': color,
+ })
+
+ mix_3 = nw.new_node(Nodes.Mix, input_kwargs={0: mix_2.outputs[2], 6: hue_saturation_value, 7: color,
+ }, attrs={'data_type': 'RGBA'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: mix_2.outputs[2], 1: log_uniform(.0002, .01)},
+ attrs={'operation': 'MULTIPLY'})
+
+ displacement = nw.new_node('ShaderNodeDisplacement', input_kwargs={'Height': multiply, 'Midlevel': 0.0000})
+
+ color = mix_3.outputs[2]
+ roughness = uniform(.0, .4)
+ roughness = nw.build_float_curve(
+ nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': log_uniform(40, 50)}),
+ [(0, roughness), (1, roughness + uniform(.0, .8))])
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': color,
+ 'Roughness': roughness,
+ 'Clearcoat': np.clip(uniform(0, 1.4), 0, 1)
+ })
+ nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf, 'Displacement': displacement})
+
+
+def apply(obj, selection=None, **kwargs):
+
+ # TODO HACK - avoiding circular imports for now
+ from infinigen.assets.materials.shelf_shaders import shader_shelves_white, shader_shelves_black_wood, shader_shelves_wood
+
+ r = uniform()
+ if r < 1 / 12:
+ shader = shader_shelves_white
+ elif r < 2 / 12:
+ shader = shader_shelves_wood
+ elif r < 3 / 12:
+ shader = shader_shelves_black_wood
+ else:
+ shader = shader_wood
+ common.apply(obj, shader, selection, **kwargs)
+
+def make_sphere():
+ return new_cube()
diff --git a/infinigen/assets/materials/woods/wood_old.py b/infinigen/assets/materials/woods/wood_old.py
new file mode 100644
index 000000000..b1c2d2e49
--- /dev/null
+++ b/infinigen/assets/materials/woods/wood_old.py
@@ -0,0 +1,76 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Mingzhe Wang, Lingjie Mei
+
+import math as ma
+
+import numpy as np
+
+from infinigen.assets.materials import common
+from infinigen.assets.materials.utils.surface_utils import clip, sample_range, sample_ratio, sample_color
+from numpy.random import uniform
+
+from infinigen.core import surface
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.util.random import log_uniform
+
+
+def shader_wood_old(nw: NodeWrangler, scale=1, offset=None, rotation=None, **kwargs):
+ # Code generated using version 2.4.3 of the node_transpiler
+
+ texture_coordinate_1 = nw.new_node(Nodes.TextureCoord)
+
+ rotation = uniform(0, ma.pi * 2, 3) if rotation is None else surface.eval_argument(nw, rotation)
+ mapping_2 = nw.new_node(Nodes.Mapping, input_kwargs={
+ 'Vector': texture_coordinate_1.outputs["Object"],
+ 'Location': surface.eval_argument(nw, offset),
+ 'Rotation': rotation
+ })
+
+ mapping_1 = nw.new_node(Nodes.Mapping, input_kwargs={
+ 'Vector': mapping_2,
+ 'Scale': np.array([log_uniform(2, 4), log_uniform(8, 16), log_uniform(2, 4)]) * scale,
+ })
+
+ musgrave_texture_2 = nw.new_node(Nodes.MusgraveTexture, input_kwargs={'Vector': mapping_1, 'Scale': 2.0},
+ attrs={'musgrave_dimensions': '4D'})
+ musgrave_texture_2.inputs['W'].default_value = sample_range(0, 5)
+ musgrave_texture_2.inputs['Scale'].default_value = sample_ratio(2.0, 3 / 4, 4 / 3)
+
+ noise_texture_1 = nw.new_node(Nodes.NoiseTexture,
+ input_kwargs={'Vector': musgrave_texture_2, 'W': 0.7, 'Scale': 10.0},
+ attrs={'noise_dimensions': '4D'})
+ noise_texture_1.inputs['W'].default_value = sample_range(0, 5)
+ noise_texture_1.inputs['Scale'].default_value = sample_ratio(5, 0.5, 2)
+
+ colorramp_2 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Fac"]})
+ colorramp_2.color_ramp.elements.new(0)
+ colorramp_2.color_ramp.elements[0].position = 0.1727
+ colorramp_2.color_ramp.elements[0].color = (0.1567, 0.0162, 0.0017, 1.0)
+ colorramp_2.color_ramp.elements[1].position = 0.4364
+ colorramp_2.color_ramp.elements[1].color = (0.2908, 0.1007, 0.0148, 1.0)
+ colorramp_2.color_ramp.elements[2].position = 0.5864
+ colorramp_2.color_ramp.elements[2].color = (0.0814, 0.0344, 0.0125, 1.0)
+ colorramp_2.color_ramp.elements[0].position += sample_range(-0.05, 0.05)
+ colorramp_2.color_ramp.elements[1].position += sample_range(-0.1, 0.1)
+ colorramp_2.color_ramp.elements[2].position += sample_range(-0.05, 0.05)
+ for e in colorramp_2.color_ramp.elements:
+ sample_color(e.color, offset=0.04)
+
+ colorramp_4 = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': noise_texture_1.outputs["Fac"]})
+ colorramp_4.color_ramp.elements[0].position = 0.0
+ colorramp_4.color_ramp.elements[0].color = (0.4855, 0.4855, 0.4855, 1.0)
+ colorramp_4.color_ramp.elements[1].position = 1.0
+ colorramp_4.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
+
+ principled_bsdf_1 = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': colorramp_2.outputs["Color"],
+ 'Roughness': colorramp_4.outputs["Color"]
+ }, attrs={'subsurface_method': 'BURLEY'})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf_1})
+
+
+def apply(obj, selection=None, scale=1, **kwargs):
+ common.apply(obj, shader_wood_old, selection, scale=scale, **kwargs)
diff --git a/infinigen/assets/materials/woods/wood_tile.py b/infinigen/assets/materials/woods/wood_tile.py
new file mode 100644
index 000000000..07ec1d9d0
--- /dev/null
+++ b/infinigen/assets/materials/woods/wood_tile.py
@@ -0,0 +1,15 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+
+
+def get_wood_tiles():
+ from . import square_wood_tile, staggered_wood_tile, crossed_wood_tile, composite_wood_tile, hexagon_wood_tile
+ return [square_wood_tile, staggered_wood_tile, crossed_wood_tile, composite_wood_tile, hexagon_wood_tile]
+
+
+def apply(obj, selection=None, vertical=False, scale=None, alternating=None, **kwargs):
+ func = np.random.choice(get_wood_tiles())
+ func.apply(obj, selection, vertical, scale, alternating, **kwargs)
diff --git a/infinigen/assets/mollusk/generate.py b/infinigen/assets/mollusk/generate.py
index 6cb37767b..103068df0 100644
--- a/infinigen/assets/mollusk/generate.py
+++ b/infinigen/assets/mollusk/generate.py
@@ -14,13 +14,15 @@
from .base import BaseMolluskFactory
from .shell import ShellBaseFactory, ScallopBaseFactory, ClamBaseFactory, MusselBaseFactory
from .snail import SnailBaseFactory, ConchBaseFactory, AugerBaseFactory, VoluteBaseFactory, NautilusBaseFactory
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
-from ..utils.decorate import assign_material, subsurface2face_size
+from infinigen.core.nodes.node_utils import build_color_ramp
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.decorate import subsurface2face_size
+from infinigen.assets.utils.misc import assign_material
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
from infinigen.core import surface
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class MolluskFactory(AssetFactory):
diff --git a/infinigen/assets/mollusk/shell.py b/infinigen/assets/mollusk/shell.py
index b038ea297..3babd5753 100644
--- a/infinigen/assets/mollusk/shell.py
+++ b/infinigen/assets/mollusk/shell.py
@@ -11,15 +11,15 @@
import infinigen.core.util.blender as butil
from infinigen.assets.creatures.util.animation.driver_repeated import repeated_driver
from infinigen.assets.mollusk.base import BaseMolluskFactory
-from infinigen.assets.utils.object import mesh2obj, data2mesh, new_circle
+from infinigen.assets.utils.object import join_objects, mesh2obj, data2mesh, new_circle
from infinigen.assets.utils.draw import shape_by_angles
-from infinigen.assets.utils.misc import log_uniform
-from infinigen.assets.utils.decorate import displace_vertices, join_objects
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.decorate import displace_vertices
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class ShellBaseFactory(BaseMolluskFactory):
diff --git a/infinigen/assets/mollusk/snail.py b/infinigen/assets/mollusk/snail.py
index 5a54bcfb8..8c34c48d0 100644
--- a/infinigen/assets/mollusk/snail.py
+++ b/infinigen/assets/mollusk/snail.py
@@ -11,12 +11,12 @@
import infinigen.core.util.blender as butil
from infinigen.assets.mollusk.base import BaseMolluskFactory
from infinigen.assets.utils.object import center, mesh2obj, data2mesh, new_empty
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class SnailBaseFactory(BaseMolluskFactory):
freq = 256
diff --git a/infinigen/assets/monocot/agave.py b/infinigen/assets/monocot/agave.py
index 5b8abbd54..bf8fd4deb 100644
--- a/infinigen/assets/monocot/agave.py
+++ b/infinigen/assets/monocot/agave.py
@@ -12,13 +12,14 @@
import infinigen.core.util.blender as butil
from infinigen.assets.monocot.growth import MonocotGrowthFactory
-from infinigen.assets.utils.decorate import add_distance_to_boundary, join_objects, displace_vertices
+from infinigen.assets.utils.decorate import distance2boundary, displace_vertices
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import cut_plane, leaf
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.surface import shaderfunc_to_material
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class AgaveMonocotFactory(MonocotGrowthFactory):
use_distance = True
@@ -45,7 +46,7 @@ def build_leaf(self, face_size):
x_anchors = 0, .2 * np.cos(self.bud_angle), uniform(1., 1.4), 1.5
y_anchors = 0, .2 * np.sin(self.bud_angle), uniform(.1, .15), 0
obj = leaf(x_anchors, y_anchors, face_size=face_size)
- distance = add_distance_to_boundary(obj)
+ distance = distance2boundary(obj)
lower = deep_clone_obj(obj)
z_offset = -log_uniform(.08, .16)
diff --git a/infinigen/assets/monocot/banana.py b/infinigen/assets/monocot/banana.py
index 356ed705b..74fa33461 100644
--- a/infinigen/assets/monocot/banana.py
+++ b/infinigen/assets/monocot/banana.py
@@ -3,21 +3,21 @@
# Authors: Lingjie Mei
-
+import bpy
import bmesh
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.decorate import displace_vertices, join_objects, read_co
+from infinigen.assets.utils.decorate import displace_vertices, read_co
from infinigen.assets.utils.draw import bezier_curve, leaf
from infinigen.assets.utils.nodegroup import geo_radius
-from infinigen.assets.utils.object import origin2lowest
+from infinigen.assets.utils.object import join_objects, origin2lowest
from infinigen.core import surface
from infinigen.assets.monocot.growth import MonocotGrowthFactory
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class BananaMonocotFactory(MonocotGrowthFactory):
diff --git a/infinigen/assets/monocot/generate.py b/infinigen/assets/monocot/generate.py
index 4ce3b293d..98b16eb63 100644
--- a/infinigen/assets/monocot/generate.py
+++ b/infinigen/assets/monocot/generate.py
@@ -16,9 +16,9 @@
from .tussock import TussockMonocotFactory
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util.math import FixedSeed
-from ..utils.decorate import join_objects
-from ..utils.mesh import polygon_angles
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.assets.utils.object import join_objects
+from infinigen.assets.utils.mesh import polygon_angles
+from infinigen.core.tagging import tag_object, tag_nodegroup
class MonocotFactory(AssetFactory):
max_cluster = 10
diff --git a/infinigen/assets/monocot/grasses.py b/infinigen/assets/monocot/grasses.py
index b8f2d65ec..0fea24138 100644
--- a/infinigen/assets/monocot/grasses.py
+++ b/infinigen/assets/monocot/grasses.py
@@ -11,11 +11,14 @@
from numpy.random import uniform
from infinigen.assets.monocot.growth import MonocotGrowthFactory
-from infinigen.assets.utils.decorate import assign_material, join_objects, remove_vertices, write_attribute, \
+from infinigen.assets.utils.decorate import remove_vertices, write_attribute, \
write_material_index
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import bezier_curve, leaf, spin
from infinigen.assets.utils.mesh import polygon_angles
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
@@ -26,7 +29,7 @@
from infinigen.core.surface import read_attr_data, shaderfunc_to_material
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class GrassesMonocotFactory(MonocotGrowthFactory):
@@ -136,7 +139,6 @@ def __init__(self, factory_seed, coarse=False):
self.leaf_range = .1, .7
def build_leaf(self, face_size):
- super().build_leaf(face_size)
x_anchors = np.array([0, uniform(.1, .2), uniform(.5, .7), 1.])
y_anchors = np.array([0, uniform(.03, .06), uniform(.03, .06), 0])
obj = leaf(x_anchors, y_anchors, face_size=face_size)
@@ -238,7 +240,7 @@ def create_asset(self, **params):
@staticmethod
def shader_ear(nw: NodeWrangler):
- color = *colorsys.hsv_to_rgb(uniform(.06, .1), uniform(.2, .5), log_uniform(.2, .5)), 1
+ color = hsv2rgba(uniform(.06, .1), uniform(.2, .5), log_uniform(.2, .5))
specular = uniform(.0, .2)
clearcoat = 0 if uniform(0, 1) < .8 else uniform(.2, .5)
noise_texture = nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': 50})
diff --git a/infinigen/assets/monocot/growth.py b/infinigen/assets/monocot/growth.py
index bc843034b..b35402ed7 100644
--- a/infinigen/assets/monocot/growth.py
+++ b/infinigen/assets/monocot/growth.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
@@ -12,13 +13,15 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.decorate import assign_material, displace_vertices, geo_extension, join_objects
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.assets.utils.nodegroup import geo_radius
from infinigen.core.placement.detail import adapt_mesh_resolution
from infinigen.core.surface import shaderfunc_to_material
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.object import data2mesh, mesh2obj, new_cube, origin2leftmost
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj, new_cube, origin2leftmost
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.placement.factory import AssetFactory, make_asset_collection
from infinigen.core.nodes.node_wrangler import NodeWrangler
@@ -26,7 +29,9 @@
from infinigen.core import surface
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.nodes.node_utils import build_color_ramp
+
class MonocotGrowthFactory(AssetFactory):
use_distance = False
@@ -51,9 +56,9 @@ def __init__(self, factory_seed, coarse=False):
self.align_factor = 0
self.align_direction = 1, 0, 0
self.base_hue = self.build_base_hue()
- self.bright_color = *colorsys.hsv_to_rgb(self.base_hue, uniform(.6, .8), log_uniform(.05, .1)), 1
- self.dark_color = *colorsys.hsv_to_rgb((self.base_hue + uniform(-.03, .03)) % 1, uniform(.8, 1.),
- log_uniform(.05, .2)), 1
+ self.bright_color = hsv2rgba(self.base_hue, uniform(.6, .8), log_uniform(.05, .1))
+ self.dark_color = hsv2rgba((self.base_hue + uniform(-.03, .03)) % 1, uniform(.8, 1.),
+ log_uniform(.05, .2))
self.material = shaderfunc_to_material(self.shader_monocot, self.dark_color, self.bright_color,
self.use_distance)
@@ -130,7 +135,7 @@ def geo_flower(nw: NodeWrangler, leaves):
'Scale': scale
})
geometry = nw.new_node(Nodes.RealizeInstances, [instances])
- geometry = nw.new_node(Nodes.StoreNamedAttribute,
+ geometry = nw.new_node(Nodes.StoreNamedAttribute,
input_kwargs={'Geometry': geometry, 'Name':'z_rotation', 'Value': z_rotation})
geometry = nw.new_node(Nodes.JoinGeometry, [[stem, geometry]])
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
diff --git a/infinigen/assets/monocot/kelp.py b/infinigen/assets/monocot/kelp.py
index 99f14773e..5851f9cc2 100644
--- a/infinigen/assets/monocot/kelp.py
+++ b/infinigen/assets/monocot/kelp.py
@@ -12,9 +12,9 @@
from infinigen.assets.creatures.util.animation.driver_repeated import repeated_driver
from infinigen.assets.monocot.growth import MonocotGrowthFactory
from infinigen.assets.utils.draw import bezier_curve, leaf
-from infinigen.assets.utils.decorate import assign_material, join_objects
-from infinigen.assets.utils.misc import log_uniform
-from infinigen.assets.utils.object import origin2leftmost
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.object import join_objects, origin2leftmost
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.detail import remesh_with_attrs
from infinigen.core.util.math import FixedSeed
diff --git a/infinigen/assets/monocot/pinecone.py b/infinigen/assets/monocot/pinecone.py
index bb60c3db4..c360da329 100644
--- a/infinigen/assets/monocot/pinecone.py
+++ b/infinigen/assets/monocot/pinecone.py
@@ -14,13 +14,15 @@
from infinigen.assets.monocot.growth import MonocotGrowthFactory
from infinigen.assets.utils.object import new_circle
from infinigen.assets.utils.draw import shape_by_angles, shape_by_xs
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.detail import remesh_with_attrs
from infinigen.core.surface import shaderfunc_to_material
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.nodes.node_utils import build_color_ramp
class PineconeFactory(MonocotGrowthFactory):
def __init__(self, factory_seed, coarse=False):
@@ -33,8 +35,8 @@ def __init__(self, factory_seed, coarse=False):
self.stem_offset = uniform(.2, .4)
self.perturb = 0
self.scale_curve = [(0, .5), (.5, uniform(.6, 1.)), (1, uniform(.1, .2))]
- self.bright_color = *colorsys.hsv_to_rgb(uniform(.02, .06), uniform(.8, 1.), .01), 1
- self.dark_color = *colorsys.hsv_to_rgb(uniform(.02, .06), uniform(.8, 1.), .005), 1
+ self.bright_color = hsv2rgba(uniform(.02, .06), uniform(.8, 1.), .01)
+ self.dark_color = hsv2rgba(uniform(.02, .06), uniform(.8, 1.), .005)
self.material = shaderfunc_to_material(self.shader_monocot, self.dark_color, self.bright_color,
self.use_distance)
diff --git a/infinigen/assets/monocot/tussock.py b/infinigen/assets/monocot/tussock.py
index ffc0493de..225427d8a 100644
--- a/infinigen/assets/monocot/tussock.py
+++ b/infinigen/assets/monocot/tussock.py
@@ -8,9 +8,9 @@
from infinigen.assets.utils.draw import leaf
from infinigen.assets.monocot.growth import MonocotGrowthFactory
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object
+from infinigen.core.tagging import tag_object
class TussockMonocotFactory(MonocotGrowthFactory):
diff --git a/infinigen/assets/monocot/veratrum.py b/infinigen/assets/monocot/veratrum.py
index e22f01a6a..a7906be1e 100644
--- a/infinigen/assets/monocot/veratrum.py
+++ b/infinigen/assets/monocot/veratrum.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
@@ -11,10 +12,12 @@
from numpy.random import uniform
from infinigen.assets.monocot.growth import MonocotGrowthFactory
-from infinigen.assets.utils.decorate import add_distance_to_boundary, assign_material, join_objects, write_attribute, \
- write_material_index
+from infinigen.assets.utils.decorate import distance2boundary, write_attribute, write_material_index
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import leaf, spin
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.factory import AssetFactory
@@ -22,7 +25,8 @@
from infinigen.core.surface import shaderfunc_to_material
from infinigen.core.util.math import FixedSeed
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+
class VeratrumMonocotFactory(MonocotGrowthFactory):
@@ -49,7 +53,7 @@ def build_base_hue():
@staticmethod
def shader_ear(nw: NodeWrangler):
- color = *colorsys.hsv_to_rgb(uniform(.1, .35), uniform(.1, .5), log_uniform(.2, .5)), 1
+ color = hsv2rgba(uniform(.1, .35), uniform(.1, .5), log_uniform(.2, .5))
specular = uniform(.0, .2)
clearcoat = 0 if uniform(0, 1) < .8 else uniform(.2, .5)
noise_texture = nw.new_node(Nodes.NoiseTexture, input_kwargs={'Scale': 50})
@@ -68,9 +72,9 @@ def build_leaf(self, face_size):
x_anchors = 0, .2 * np.cos(self.bud_angle), uniform(.6, .7), .8
y_anchors = 0, .2 * np.sin(self.bud_angle), uniform(.06, .1), 0
obj = leaf(x_anchors, y_anchors, face_size=face_size)
- distance = add_distance_to_boundary(obj)
+ distance = distance2boundary(obj)
- vg = obj.vertex_groups['distance']
+ vg = obj.vertex_groups.new(name='distance')
weights = np.cos(self.freq * distance) ** 4
for i, w in enumerate(weights):
vg.add([i], w, 'REPLACE')
@@ -86,7 +90,7 @@ def create_asset(self, **params):
self.decorate_monocot(obj)
assign_material(obj, [self.material, self.branch_material])
- write_material_index(obj, surface.read_attr_data(obj, 'ear', 'FACE').astype(int)[:, 0])
+ write_material_index(obj, surface.read_attr_data(obj, 'ear', 'FACE').astype(int))
tag_object(obj, 'veratrum')
return obj
@@ -128,10 +132,10 @@ def __init__(self, factory_seed, coarse=False):
self.leaf_range = 0, .98
def build_leaf(self, face_size):
- x_achors = 0, .04, .06, .04, 0
+ x_anchors = 0, .04, .06, .04, 0
y_anchors = 0, .01, 0, -.01, 0
z_anchors = 0, - .01, -.01, -.006, 0
- anchors = [x_achors, y_anchors, z_anchors]
+ anchors = [x_anchors, y_anchors, z_anchors]
obj = spin(anchors, [0, 2, 4], dupli=True, loop=True, resolution=np.random.randint(3, 5),
axis=(1, 0, 0))
butil.modify_mesh(obj, 'WELD', merge_threshold=face_size / 2)
diff --git a/infinigen/assets/mushroom/cap.py b/infinigen/assets/mushroom/cap.py
index 8a36d7b8c..49374adcf 100644
--- a/infinigen/assets/mushroom/cap.py
+++ b/infinigen/assets/mushroom/cap.py
@@ -10,12 +10,14 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.decorate import assign_material, displace_vertices, geo_extension, join_objects, \
+from infinigen.assets.utils.decorate import displace_vertices, geo_extension, \
subsurface2face_size
+from infinigen.assets.utils.misc import assign_material
from infinigen.assets.utils.draw import spin
from infinigen.assets.utils.mesh import polygon_angles
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
-from infinigen.assets.utils.object import data2mesh, mesh2obj
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.detail import remesh_with_attrs
@@ -23,7 +25,8 @@
from infinigen.core import surface
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.nodes.node_utils import build_color_ramp
class MushroomCapFactory(AssetFactory):
@@ -177,8 +180,8 @@ def geo_xyz(nw: NodeWrangler):
m = nw.new_node(Nodes.AttributeStatistic, [geometry, None, component]).outputs['Max']
geometry = nw.new_node(Nodes.StoreNamedAttribute,
input_kwargs={
- 'Geometry': geometry,
- 'Name': name,
+ 'Geometry': geometry,
+ 'Name': name,
'Value': nw.scalar_divide(component, m)
})
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
@@ -190,7 +193,7 @@ def geo_morel(nw: NodeWrangler):
'Scale': uniform(15, 20),
'Randomness': uniform(.5, 1)
}, attrs={'feature': 'DISTANCE_TO_EDGE'}), .05)
- geometry = nw.new_node(Nodes.StoreNamedAttribute,
+ geometry = nw.new_node(Nodes.StoreNamedAttribute,
input_kwargs={'Geometry':geometry, 'Name':'morel', 'Value': selection})
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
@@ -225,8 +228,9 @@ def create_asset(self, face_size, **params) -> bpy.types.Object:
assign_material(obj, self.material_cap)
if self.is_morel:
- obj.data.attributes.active = obj.data.attributes['morel']
- bpy.ops.geometry.attribute_convert(mode='VERTEX_GROUP')
+ with butil.SelectObjects(obj):
+ surface.set_active(obj,'morel')
+ bpy.ops.geometry.attribute_convert(mode='VERTEX_GROUP')
butil.modify_mesh(obj, 'DISPLACE', vertex_group='morel', strength=.04, mid_level=.7)
if self.gill_config is not None:
@@ -252,12 +256,12 @@ def create_asset(self, face_size, **params) -> bpy.types.Object:
@staticmethod
def shader_voronoi(nw: NodeWrangler, base_hue):
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .8), log_uniform(.05, .2)), 1
+ bright_color = hsv2rgba(base_hue, uniform(.4, .8), log_uniform(.05, .2))
dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.01, .05)), 1
subsurface_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.05, .2)), 1
- light_color = *colorsys.hsv_to_rgb(base_hue, uniform(0, .1), uniform(.2, .8)), 1
+ light_color = hsv2rgba(base_hue, uniform(0, .1), uniform(.2, .8))
anchors = [.0, .3, .6, 1.] if uniform(0, 1) < .5 else [.0, .4, .7, 1.]
color = build_color_ramp(nw, nw.musgrave(500), anchors,
[dark_color, dark_color, bright_color, bright_color])
@@ -296,12 +300,12 @@ def shader_voronoi(nw: NodeWrangler, base_hue):
@staticmethod
def shader_speckle(nw: NodeWrangler, base_hue):
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .8), log_uniform(.05, .2)), 1
+ bright_color = hsv2rgba(base_hue, uniform(.4, .8), log_uniform(.05, .2))
dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.01, .05)), 1
subsurface_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.05, .2)), 1
- light_color = *colorsys.hsv_to_rgb(base_hue, uniform(0, .1), uniform(.2, .8)), 1
+ light_color = hsv2rgba(base_hue, uniform(0, .1), uniform(.2, .8))
anchors = [.0, .3, .6, 1.] if uniform(0, 1) < .5 else [.0, .4, .7, 1.]
color = build_color_ramp(nw, nw.musgrave(500), anchors,
[dark_color, dark_color, bright_color, bright_color])
@@ -325,12 +329,12 @@ def shader_speckle(nw: NodeWrangler, base_hue):
@staticmethod
def shader_noise(nw: NodeWrangler, base_hue):
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .8), log_uniform(.05, .2)), 1
+ bright_color = hsv2rgba(base_hue, uniform(.4, .8), log_uniform(.05, .2))
dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.01, .05)), 1
subsurface_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.05, .2)), 1
- light_color = *colorsys.hsv_to_rgb(base_hue, uniform(0, .1), uniform(.2, .8)), 1
+ light_color = hsv2rgba(base_hue, uniform(0, .1), uniform(.2, .8))
anchors = [.0, .3, .6, 1.] if uniform(0, 1) < .5 else [.0, .4, .7, 1.]
color = build_color_ramp(nw, nw.musgrave(500), anchors,
[dark_color, dark_color, bright_color, bright_color])
@@ -356,10 +360,10 @@ def shader_noise(nw: NodeWrangler, base_hue):
@staticmethod
def shader_cap(nw: NodeWrangler, base_hue):
- bright_color = *colorsys.hsv_to_rgb(base_hue, uniform(.6, .8), log_uniform(.05, .2)), 1
+ bright_color = hsv2rgba(base_hue, uniform(.6, .8), log_uniform(.05, .2))
dark_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.4, .8),
log_uniform(.01, .05)), 1
- light_color = *colorsys.hsv_to_rgb(base_hue, uniform(0, .1), uniform(.6, .8)), 1
+ light_color = hsv2rgba(base_hue, uniform(0, .1), uniform(.6, .8))
subsurface_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.05, .05)) % 1, uniform(.6, .8),
log_uniform(.05, .2)), 1
diff --git a/infinigen/assets/mushroom/generate.py b/infinigen/assets/mushroom/generate.py
index 369b39904..1f4c49459 100644
--- a/infinigen/assets/mushroom/generate.py
+++ b/infinigen/assets/mushroom/generate.py
@@ -13,12 +13,12 @@
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.util.math import FixedSeed
from .growth import MushroomGrowthFactory
-from infinigen.assets.utils.decorate import join_objects
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from ..utils.misc import log_uniform
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.util.random import log_uniform
+from infinigen.core.tagging import tag_object, tag_nodegroup
class MushroomFactory(AssetFactory):
max_cluster = 10
diff --git a/infinigen/assets/mushroom/growth.py b/infinigen/assets/mushroom/growth.py
index ba60c1194..4164986ec 100644
--- a/infinigen/assets/mushroom/growth.py
+++ b/infinigen/assets/mushroom/growth.py
@@ -13,14 +13,14 @@
from infinigen.core.util.math import FixedSeed
from .cap import MushroomCapFactory
from .stem import MushroomStemFactory
-from infinigen.assets.utils.object import origin2lowest
+from infinigen.assets.utils.object import join_objects, origin2lowest
from infinigen.core.placement.factory import AssetFactory
-from ..utils.decorate import join_objects
-from ..utils.misc import build_color_ramp, log_uniform
-
+from infinigen.assets.utils.object import join_objects
+from infinigen.core.util.random import log_uniform
+from infinigen.core.nodes.node_utils import build_color_ramp
class MushroomGrowthFactory(AssetFactory):
-
+
def __init__(self, factory_seed, coarse=False):
super().__init__(factory_seed, coarse)
with FixedSeed(factory_seed):
diff --git a/infinigen/assets/mushroom/stem.py b/infinigen/assets/mushroom/stem.py
index 28846a49a..d18cfd36f 100644
--- a/infinigen/assets/mushroom/stem.py
+++ b/infinigen/assets/mushroom/stem.py
@@ -8,10 +8,12 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.decorate import assign_material, geo_extension, join_objects, subsurface2face_size
+from infinigen.assets.utils.decorate import geo_extension, subsurface2face_size
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import spin
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.detail import remesh_with_attrs
@@ -19,7 +21,7 @@
from infinigen.core import surface
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class MushroomStemFactory(AssetFactory):
diff --git a/infinigen/assets/organizer/__init__.py b/infinigen/assets/organizer/__init__.py
new file mode 100644
index 000000000..4eeef6039
--- /dev/null
+++ b/infinigen/assets/organizer/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+
+from .basket import BasketBaseFactory
+from .hook import HookBaseFactory, SpatulaOnHookBaseFactory
+from .plate_rack import PlateRackBaseFactory, PlateOnRackBaseFactory
diff --git a/infinigen/assets/organizer/basket.py b/infinigen/assets/organizer/basket.py
new file mode 100644
index 000000000..5e711c113
--- /dev/null
+++ b/infinigen/assets/organizer/basket.py
@@ -0,0 +1,306 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+from infinigen.assets.materials.plastics.plastic_rough import shader_rough_plastic
+
+
+@node_utils.to_nodegroup('nodegroup_holes', singleton=False, type='GeometryNodeTree')
+def nodegroup_holes(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Value1', 0.5000),
+ ('NodeSocketFloat', 'Value2', 0.5000),
+ ('NodeSocketFloat', 'Value3', 0.5000),
+ ('NodeSocketFloat', 'Value4', 0.5000),
+ ('NodeSocketFloat', 'Value5', 0.5000),
+ ('NodeSocketFloat', 'Value6', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value3"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value1"], 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value6"], 1: 0.0000})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value4"], 1: 0.0000})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: group_input.outputs["Value2"]})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: add_3}, attrs={'operation': 'DIVIDE'})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: add_3}, attrs={'operation': 'DIVIDE'})
+
+ grid = nw.new_node(Nodes.MeshGrid,
+ input_kwargs={'Size X': subtract, 'Size Y': subtract_1, 'Vertices X': divide, 'Vertices Y': divide_1})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': grid.outputs["Mesh"], 'Name': 'uv_map', 3: grid.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value5"], 1: 0.0000})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: 0.1})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_5, 'Y': add_2, 'Z': add_2})
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_3})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints, input_kwargs={'Points': transform_1, 'Instance': store_named_attribute_1})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ divide_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: add_3}, attrs={'operation': 'DIVIDE'})
+
+ grid_1 = nw.new_node(Nodes.MeshGrid,
+ input_kwargs={'Size X': subtract_2, 'Size Y': subtract, 'Vertices X': divide_2, 'Vertices Y': divide})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': grid_1.outputs["Mesh"], 'Name': 'uv_map', 3: grid_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_2, 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: 0.1})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': add_6, 'Z': add_2})
+
+ cube_3 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_4})
+
+ store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_3.outputs["Mesh"], 'Name': 'uv_map', 3: cube_3.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints, input_kwargs={'Points': transform_2, 'Instance': store_named_attribute_3})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Instances1': instance_on_points, 'Instances2': instance_on_points_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_handle_hole', singleton=False, type='GeometryNodeTree')
+def nodegroup_handle_hole(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'X', 0.0000),
+ ('NodeSocketFloat', 'Z', 0.0000),
+ ('NodeSocketFloat', 'Value', 0.5000),
+ ('NodeSocketFloat', 'Value2', 0.5000),
+ ('NodeSocketInt', 'Level', 0)])
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["X"], 'Y': 1.0000, 'Z': group_input.outputs["Z"]})
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_3})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map', 3: cube_2.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subdivide_mesh_2 = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': store_named_attribute})
+
+ subdivision_surface_2 = nw.new_node(Nodes.SubdivisionSurface,
+ input_kwargs={'Mesh': subdivide_mesh_2, 'Level': group_input.outputs["Level"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["Value2"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': subtract})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': subdivision_surface_2, 'Translation': combine_xyz_4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1}, attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ depth = nw.new_node(Nodes.Value, label='depth')
+ depth.outputs[0].default_value = kwargs['depth']
+
+ width = nw.new_node(Nodes.Value, label='width')
+ width.outputs[0].default_value = kwargs['width']
+
+ height = nw.new_node(Nodes.Value, label='height')
+ height.outputs[0].default_value = kwargs['height']
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': depth, 'Y': width, 'Z': height})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': store_named_attribute, 'Level': 2})
+
+ sub_level = nw.new_node(Nodes.Integer, label='sub_level')
+ sub_level.integer = kwargs['frame_sub_level']
+
+ subdivision_surface = nw.new_node(Nodes.SubdivisionSurface,
+ input_kwargs={'Mesh': subdivide_mesh, 'Level': sub_level})
+
+ differences = []
+
+ if kwargs['has_handle']:
+ hole_depth = nw.new_node(Nodes.Value, label='hole_depth')
+ hole_depth.outputs[0].default_value = kwargs['handle_depth']
+
+ hole_height = nw.new_node(Nodes.Value, label='hole_height')
+ hole_height.outputs[0].default_value = kwargs['handle_height']
+
+ hole_dist = nw.new_node(Nodes.Value, label='hole_dist')
+ hole_dist.outputs[0].default_value = kwargs['handle_dist_to_top']
+
+ handle_level = nw.new_node(Nodes.Integer, label='handle_level')
+ handle_level.integer = kwargs['handle_sub_level']
+ handle_hole = nw.new_node(nodegroup_handle_hole().name,
+ input_kwargs={'X': hole_depth, 'Z': hole_height, 'Value': height, 'Value2': hole_dist,
+ 'Level': handle_level})
+ differences.append(handle_hole)
+
+ thickness = nw.new_node(Nodes.Value, label='thickness')
+ thickness.outputs[0].default_value = kwargs['thickness']
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: depth, 1: thickness}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: width, 1: thickness}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': subtract_1, 'Z': height})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube_1.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ subdivide_mesh_1 = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': store_named_attribute_1, 'Level': 2})
+
+ subdivision_surface_1 = nw.new_node(Nodes.SubdivisionSurface,
+ input_kwargs={'Mesh': subdivide_mesh_1, 'Level': sub_level})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: thickness, 2: 0.2500}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': subdivision_surface_1, 'Translation': combine_xyz_2})
+
+ if kwargs['has_holes']:
+ gap_size = nw.new_node(Nodes.Value, label='gap_size')
+ gap_size.outputs[0].default_value = kwargs['hole_gap_size']
+
+ hole_edge_gap = nw.new_node(Nodes.Value, label='hole_edge_gap')
+ hole_edge_gap.outputs[0].default_value = kwargs['hole_edge_gap']
+
+ hole_size = nw.new_node(Nodes.Value, label='hole_size')
+ hole_size.outputs[0].default_value = kwargs['hole_size']
+ holes = nw.new_node(nodegroup_holes().name,
+ input_kwargs={'Value1': height, 'Value2': gap_size, 'Value3': hole_edge_gap,
+ 'Value4': hole_size, 'Value5': depth, 'Value6': width})
+ differences.extend([holes.outputs["Instances1"], holes.outputs["Instances2"]])
+
+ difference = nw.new_node(Nodes.MeshBoolean,
+ input_kwargs={'Mesh 1': subdivision_surface, 'Mesh 2': [transform] + differences})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': difference.outputs["Mesh"]})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: height}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': realize_instances, 'Translation': combine_xyz_3})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': transform_geometry,
+ 'Material': surface.shaderfunc_to_material(shader_rough_plastic)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material},
+ attrs={'is_active_output': True})
+
+
+class BasketBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(BasketBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('depth', None) is None:
+ params['depth'] = uniform(0.15, 0.4)
+ if params.get('width', None) is None:
+ params['width'] = uniform(0.2, 0.6)
+ if params.get('height', None) is None:
+ params['height'] = uniform(0.06, 0.24)
+ if params.get('frame_sub_level', None) is None:
+ params['frame_sub_level'] = np.random.choice([0, 3], p=[0.5, 0.5])
+ if params.get('thickness', None) is None:
+ params['thickness'] = uniform(0.001, 0.005)
+
+ if params.get('has_handle', None) is None:
+ params['has_handle'] = np.random.choice([True, False], p=[0.8, 0.2])
+ if params.get('handle_sub_level', None) is None:
+ params['handle_sub_level'] = np.random.choice([0, 1, 2], p=[0.2, 0.4, 0.4])
+ if params.get('handle_depth', None) is None:
+ params['handle_depth'] = params['depth'] * uniform(0.2, 0.4)
+ if params.get('handle_height', None) is None:
+ params['handle_height'] = params['height'] * uniform(0.1, 0.25)
+ if params.get('handle_dist_to_top', None) is None:
+ params['handle_dist_to_top'] = (params['handle_height'] * 0.5 +
+ params['height'] * uniform(0.08, 0.15))
+
+ if params.get('has_holes', None) is None:
+ if params['height'] < 0.12:
+ params['has_holes'] = False
+ else:
+ params['has_holes'] = np.random.choice([True, False], p=[0.5, 0.5])
+ if params.get('hole_size', None) is None:
+ params['hole_size'] = uniform(0.005, 0.01)
+ if params.get('hole_gap_size', None) is None:
+ params['hole_gap_size'] = params['hole_size'] * uniform(0.8, 1.1)
+ if params.get('hole_edge_gap', None) is None:
+ params['hole_edge_gap'] = uniform(0.04, 0.06)
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
diff --git a/infinigen/assets/organizer/hook.py b/infinigen/assets/organizer/hook.py
new file mode 100644
index 000000000..2403dd132
--- /dev/null
+++ b/infinigen/assets/organizer/hook.py
@@ -0,0 +1,387 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging
+
+import bpy
+from infinigen.assets.materials import shader_rough_plastic, shader_brushed_metal
+from infinigen.assets.materials.plastics.plastic_rough import shader_rough_plastic
+
+
+def hook_geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ hook_num = nw.new_node(Nodes.Integer, label='hook_num')
+ hook_num.integer = kwargs["num_hook"]
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: hook_num, 1: -1.0000})
+
+ hook_gap = nw.new_node(Nodes.Value, label='hook_gap')
+ hook_gap.outputs[0].default_value = kwargs["hook_gap"]
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: hook_gap, 1: add}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_1})
+
+ mesh_line = nw.new_node(Nodes.MeshLine,
+ input_kwargs={'Count': add, 'Start Location': combine_xyz_2, 'Offset': combine_xyz_1},
+ attrs={'mode': 'END_POINTS'})
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Start': (0.0000, 0.0000, 0.0000),
+ 'Start Handle': (0.0000, 0.0000, kwargs["init_handle"]),
+ 'End Handle': kwargs["curve_handle"],
+ 'End': kwargs["curve_end_point"]})
+
+ curve_line = nw.new_node(Nodes.CurveLine)
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [bezier_segment, curve_line]})
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Factor': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], [(0.0000, 0.8), (0.5, 0.8), (1.0000, 0.8)])
+
+ raduis = nw.new_node(Nodes.Value, label='raduis')
+ raduis.outputs[0].default_value = kwargs['hook_radius']
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: float_curve, 1: raduis}, attrs={'operation': 'MULTIPLY'})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': join_geometry_3, 'Radius': multiply_3})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle,
+ input_kwargs={'Resolution': kwargs['hook_resolution'],
+ 'Point 1': (1.0000, 0.0000, 0.0000),
+ 'Point 3': (-1.0000, 0.0000, 0.0000)},
+ attrs={'mode': 'POINTS'})
+
+ hook_reshape = nw.new_node(Nodes.Vector, label='hook_reshape')
+ hook_reshape.vector = (1.0000, 1.0000, 1.0000)
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Scale': hook_reshape})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': transform_geometry_2,
+ 'Fill Caps': True})
+
+ hook_size = nw.new_node(Nodes.Value, label='hook_size')
+ hook_size.outputs[0].default_value = kwargs['hook_size']
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh, 'Scale': hook_size})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': transform_geometry})
+
+ merge_by_distance_1 = nw.new_node(Nodes.MergeByDistance, input_kwargs={'Geometry': realize_instances_1})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': mesh_line, 'Instance': merge_by_distance_1})
+
+ scale_instances = nw.new_node(Nodes.ScaleInstances, input_kwargs={'Instances': instance_on_points})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': scale_instances,
+ 'Material': surface.shaderfunc_to_material(shader_brushed_metal)})
+
+ board_side_gap = nw.new_node(Nodes.Value, label='board_side_gap')
+ board_side_gap.outputs[0].default_value = kwargs['board_side_gap']
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: board_side_gap})
+
+ board_thickness = nw.new_node(Nodes.Value, label='board_thickness')
+ board_thickness.outputs[0].default_value = kwargs['board_thickness']
+
+ board_height = nw.new_node(Nodes.Value, label='board_height')
+ board_height.outputs[0].default_value = kwargs['board_height']
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': board_thickness, 'Z': board_height})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: board_thickness, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: board_height}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: hook_size, 1: multiply_5}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_4, 'Z': subtract})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Translation': combine_xyz_3})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': transform_geometry_1,
+ 'Material': surface.shaderfunc_to_material(shader_rough_plastic)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_2})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry_3},
+ attrs={'is_active_output': True})
+
+
+def spatula_geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ handle_length = nw.new_node(Nodes.Value, label='handle_length')
+ handle_length.outputs[0].default_value = kwargs['handle_length']
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': handle_length})
+
+ mesh_line = nw.new_node(Nodes.MeshLine, input_kwargs={'Count': 64, 'Offset': combine_xyz}, attrs={'mode': 'END_POINTS'})
+
+ mesh_to_curve = nw.new_node(Nodes.MeshToCurve, input_kwargs={'Mesh': mesh_line})
+
+ handle_radius = nw.new_node(Nodes.Value, label='handle_radius')
+ handle_radius.outputs[0].default_value = kwargs['handle_radius']
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], kwargs['handle_control_points'])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: handle_radius, 1: float_curve}, attrs={'operation': 'MULTIPLY'})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': mesh_to_curve, 'Radius': multiply})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle)
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': curve_circle.outputs["Curve"], 'Fill Caps': True})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh,
+ 'Scale': (kwargs['handle_ratio'], 1.0, 1.0)})
+
+ hole_radius = nw.new_node(Nodes.Value, label='hole_radius')
+ hole_radius.outputs[0].default_value = kwargs['hole_radius']
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Radius': hole_radius, 'Depth': 0.1000})
+
+ hole_place_ratio = nw.new_node(Nodes.Value, label='hole_placement')
+ hole_place_ratio.outputs[0].default_value = kwargs['hole_placement']
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: handle_length, 1: hole_place_ratio}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_1,
+ 'Rotation': (0.0000, 1.5708, 0.0000), 'Scale': (kwargs['hole_ratio'], 1.0000, 1.0000)})
+
+ difference = nw.new_node(Nodes.MeshBoolean, input_kwargs={'Mesh 1': transform_geometry, 'Mesh 2': transform_geometry_1})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': (kwargs['plate_thickness'], kwargs['plate_width'], kwargs['plate_length']),
+ 'Vertices X': 4, 'Vertices Y': 4, 'Vertices Z': 4})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cube.outputs["Mesh"],
+ 'Translation': (0.0000, 0.0000, -kwargs['plate_length'] / 2.)})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [difference.outputs["Mesh"], transform_geometry_3]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_2})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': triangulate, 'Translation': combine_xyz_2})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': transform_geometry_2,
+ 'Material': surface.shaderfunc_to_material(shader_rough_plastic)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material},
+ attrs={'is_active_output': True})
+
+
+class HookBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(HookBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_hang_points(self, params):
+ # compute the lowest point in the bezier curve
+ x = params['init_handle']
+ y = params['curve_handle'][2] - params['init_handle']
+ z = params['curve_end_point'][2] - params['curve_handle'][2]
+
+ t1 = (x - y + np.sqrt(y ** 2 - x * z)) / (x + z - 2 * y)
+ t2 = (x - y - np.sqrt(y ** 2 - x * z)) / (x + z - 2 * y)
+
+ t = 0
+ if t1 >= 0 and t1 <= 1:
+ t = max(t1, t)
+ if t2 >= 0 and t2 <= 1:
+ t = max(t2, t)
+ if t == 0:
+ t = 0.5
+
+ # get x, z coordinate
+ alpha1 = 3 * ((1 - t) ** 2) * t
+ alpha2 = 3 * (1 - t) * (t ** 2)
+ alpha3 = t ** 3
+
+ z = alpha1 * params['init_handle'] + alpha2 * params['curve_handle'][-1] + alpha3 * params['curve_end_point'][-1]
+ x = alpha2 * params['curve_handle'][-2] + alpha3 * params['curve_end_point'][-2]
+
+ ys = []
+ total_length = params['board_side_gap'] + (params['num_hook'] - 1) * params['hook_gap']
+ for i in range(params['num_hook']):
+ y = - total_length / 2. + params['board_side_gap'] / 2. + i * params['hook_gap']
+ ys.append(y)
+
+ hang_points = []
+ for y in ys:
+ hang_points.append((x * params['hook_size'], y, z * params['hook_size']))
+
+ return hang_points
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('num_hook', None) is None:
+ params['num_hook'] = randint(3, 6)
+ if params.get('hook_size', None) is None:
+ params['hook_size'] = uniform(0.05, 0.1)
+ if params.get('hook_radius', None) is None:
+ params['hook_radius'] = uniform(0.002, 0.004) / params['hook_size']
+ else:
+ params['hook_radius'] = params['hook_radius'] / params['hook_size']
+
+ if params.get('hook_resolution', None) is None:
+ params['hook_resolution'] = np.random.choice([4, 32], p=[0.5, 0.5])
+
+ if params.get("hook_gap", None) is None:
+ params["hook_gap"] = uniform(0.04, 0.08)
+ if params.get('board_height', None) is None:
+ params['board_height'] = params['hook_size'] + uniform(-0.02, 0.01)
+ if params.get('board_thickness', None) is None:
+ params['board_thickness'] = uniform(0.005, 0.015)
+ if params.get('board_side_gap', None) is None:
+ params['board_side_gap'] = uniform(0.03, 0.05)
+
+ params['init_handle'] = uniform(-0.15, -0.25)
+ params["curve_handle"] = (0, uniform(0.15, 0.35), uniform(-0.15, -0.35))
+ params["curve_end_point"] = (0, uniform(0.35, 0.55), uniform(-0.05, 0.15))
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, hook_geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ hang_points = self.get_hang_points(obj_params)
+
+ return obj, hang_points
+
+
+class SpatulaBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(SpatulaBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+
+ if params.get('hole_radius', None) is None:
+ params['hole_radius'] = uniform(0.003, 0.008)
+ if params.get('hole_placement', None) is None:
+ params['hole_placement'] = uniform(0.75, 0.9)
+ if params.get('hole_ratio', None) is None:
+ params['hole_ratio'] = uniform(0.8, 2.0)
+
+ if params.get('handle_length', None) is None:
+ params['handle_length'] = uniform(0.15, 0.25)
+
+ if params.get("handle_ratio", None) is None:
+ params["handle_ratio"] = uniform(0.1, 0.4)
+ if params.get("handle_control_points", None) is None:
+ params["handle_control_points"] = [(0, 0.5), (0.5, uniform(0.45, 0.65)), (1.0, uniform(0.4, 0.6))]
+ if params.get("handle_radius", None) is None:
+ params["handle_radius"] = (params['hole_radius'] / params["handle_control_points"][0][1]) / uniform(0.6, 0.8)
+
+ if params.get('plate_thickness', None) is None:
+ params['plate_thickness'] = uniform(0.005, 0.01)
+ if params.get('plate_width', None) is None:
+ params['plate_width'] = uniform(0.04, 0.06)
+ if params.get('plate_length', None) is None:
+ params['plate_length'] = uniform(0.05, 0.08)
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, spatula_geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class SpatulaOnHookBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(SpatulaOnHookBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ self.hook_fac = HookBaseFactory(factory_seed, params=params)
+ self.spatula_fac = SpatulaBaseFactory(factory_seed, params=params)
+
+ def get_asset_params(self, i):
+ if self.params.get('hook_radius', None) is None:
+ r = uniform(0.002, 0.0035)
+ self.hook_fac.params['hook_radius'] = r
+ self.spatula_fac.params['hole_radius'] = r / uniform(0.3, 0.6)
+
+ def create_asset(self, i, **params):
+
+ self.get_asset_params(i)
+ hook, hang_points = self.hook_fac.create_asset(i)
+ spatula = self.spatula_fac.create_asset(i)
+
+ spatula.location = hang_points[0]
+ butil.apply_transform(spatula, loc=True)
+
+ return hook
+
+
+
+
+
+
+
diff --git a/infinigen/assets/organizer/plate_rack.py b/infinigen/assets/organizer/plate_rack.py
new file mode 100644
index 000000000..3894741fa
--- /dev/null
+++ b/infinigen/assets/organizer/plate_rack.py
@@ -0,0 +1,335 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+from infinigen.assets.materials import shader_wood
+from infinigen.assets.materials.plastics.plastic_rough import shader_rough_plastic
+
+
+@node_utils.to_nodegroup('nodegroup_plate_rack_connect', singleton=False, type='GeometryNodeTree')
+def nodegroup_plate_rack_connect(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'Value1', 0.5000),
+ ('NodeSocketFloat', 'Value', 0.5000)])
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Value1"], 1: 2.0000, 2: -0.0020},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Radius': group_input.outputs["Radius"], 'Depth': multiply_add})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply_add_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Value"], 2: -uniform(0.02, 0.045)},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add_1})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz,
+ 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_rack_cyn', singleton=False, type='GeometryNodeTree')
+def nodegroup_rack_cyn(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'Value', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value"], 1: 0.0000})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Radius': group_input.outputs["Radius"], 'Depth': add})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: add, 2: 0.0010}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_add})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_rack_base', singleton=False, type='GeometryNodeTree')
+def nodegroup_rack_base(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Instance', None),
+ ('NodeSocketFloat', 'Value1', 0.5000),
+ ('NodeSocketFloat', 'Value2', 0.5000),
+ ('NodeSocketFloat', 'Value3', 0.5000),
+ ('NodeSocketInt', 'Count', 10)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value1"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value2"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_1})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Value3"], 1: 0.0000})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_2})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: add, 2: -0.0150}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add, 'Y': add_2})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': add_2})
+
+ mesh_line = nw.new_node(Nodes.MeshLine,
+ input_kwargs={'Count': group_input.outputs["Count"], 'Start Location': combine_xyz_2,
+ 'Offset': combine_xyz_3},
+ attrs={'mode': 'END_POINTS'})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': mesh_line, 'Instance': group_input.outputs["Instance"]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Base': transform, 'Racks': realize_instances},
+ attrs={'is_active_output': True})
+
+
+def rack_geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ rack_radius = nw.new_node(Nodes.Value, label='rack_radius')
+ rack_radius.outputs[0].default_value = kwargs['rack_radius']
+
+ rack_height = nw.new_node(Nodes.Value, label='rack_height')
+ rack_height.outputs[0].default_value = kwargs['rack_height']
+
+ rack_cyn = nw.new_node(nodegroup_rack_cyn().name, input_kwargs={'Radius': rack_radius, 'Value': rack_height})
+
+ base_length = nw.new_node(Nodes.Value, label='base_length')
+ base_length.outputs[0].default_value = kwargs['base_length']
+
+ base_width = nw.new_node(Nodes.Value, label='base_width')
+ base_width.outputs[0].default_value = kwargs['base_width']
+
+ base_gap = nw.new_node(Nodes.Value, label='base_gap')
+ base_gap.outputs[0].default_value = kwargs['base_gap']
+
+ integer = nw.new_node(Nodes.Integer)
+ integer.integer = kwargs['num_rack']
+
+ rack_base = nw.new_node(nodegroup_rack_base().name,
+ input_kwargs={'Instance': rack_cyn, 'Value1': base_length, 'Value2': base_width,
+ 'Value3': base_gap, 'Count': integer})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [rack_base.outputs["Base"], rack_base.outputs["Racks"]]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Scale': (1.0000, -1.0000, 1.0000)})
+
+ plate_rack_connect = nw.new_node(nodegroup_plate_rack_connect().name,
+ input_kwargs={'Radius': rack_radius, 'Value1': base_gap, 'Value': base_length})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_1, join_geometry, plate_rack_connect]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: base_width}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': transform})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': triangulate,
+ 'Material': surface.shaderfunc_to_material(shader_wood)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material},
+ attrs={'is_active_output': True})
+
+
+def plate_geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ radius = nw.new_node(Nodes.Value, label='radius')
+ radius.outputs[0].default_value = kwargs['radius']
+
+ thickness = nw.new_node(Nodes.Value, label='thickness')
+ thickness.outputs[0].default_value = kwargs['thickness']
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': radius, 'Depth': thickness})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': radius})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz,
+ 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': transform_geometry})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={'Geometry': triangulate,
+ 'Material': surface.shaderfunc_to_material(shader_rough_plastic)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material},
+ attrs={'is_active_output': True})
+
+
+class PlateRackBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(PlateRackBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_place_points(self, params):
+ # compute the lowest point in the bezier curve
+ xs = []
+ for i in range(params['num_rack']-1):
+ l = params['base_length']
+ d = (l - 0.03) / (params['num_rack']-1)
+ x = - l / 2. + 0.015 + (i + 0.5) * d
+ xs.append(x)
+
+ y = 0
+ z = params['base_width']
+
+ place_points = []
+ for x in xs:
+ place_points.append((x, y, z))
+
+ return place_points
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('num_rack', None) is None:
+ params['num_rack'] = randint(3, 7)
+ if params.get('rack_radius', None) is None:
+ params['rack_radius'] = uniform(0.0025,0.006)
+ if params.get('rack_height', None) is None:
+ params['rack_height'] = uniform(0.08, 0.15)
+ if params.get('base_length', None) is None:
+ params['base_length'] = (params['num_rack'] - 1) * uniform(0.03, 0.06) + 0.03
+ if params.get('base_gap', None) is None:
+ params['base_gap'] = uniform(0.05, 0.08)
+ if params.get('base_width', None) is None:
+ params['base_width'] = uniform(0.015, 0.03)
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, rack_geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ place_points = self.get_place_points(obj_params)
+
+ return obj, place_points
+
+
+class PlateBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(PlateBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('radius', None) is None:
+ params['radius'] = uniform(0.15, 0.25)
+ if params.get('thickness', None) is None:
+ params['thickness'] = uniform(0.01, 0.025)
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, plate_geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class PlateOnRackBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(PlateOnRackBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ self.rack_fac = PlateRackBaseFactory(factory_seed, params=params)
+ self.plate_fac = PlateBaseFactory(factory_seed, params=params)
+
+ def get_asset_params(self, i):
+ if self.params.get('base_gap', None) is None:
+ d = uniform(0.05, 0.08)
+ self.rack_fac.params['base_gap'] = d
+ self.plate_fac.params['radius'] = d + uniform(0.025, 0.06)
+
+ def create_asset(self, i, **params):
+
+ self.get_asset_params(i)
+ rack, place_points = self.rack_fac.create_asset(i)
+ plate = self.plate_fac.create_asset(i)
+
+ plate.location = place_points[0]
+ butil.apply_transform(plate, loc=True)
+
+ return plate
diff --git a/infinigen/assets/rocks/blender_rock.py b/infinigen/assets/rocks/blender_rock.py
index b7a89c633..76fb9d1ce 100644
--- a/infinigen/assets/rocks/blender_rock.py
+++ b/infinigen/assets/rocks/blender_rock.py
@@ -13,7 +13,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.util import blender as butil
from infinigen.core.placement.factory import AssetFactory
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
bpy.ops.preferences.addon_enable(module='add_mesh_extra_objects')
@@ -50,8 +50,12 @@ def create_asset(self, **params):
pass
except RuntimeError:
pass
+
obj = bpy.context.active_object
bpy.ops.object.shade_flat()
+
+ butil.apply_modifiers(obj)
+
tag_object(obj, 'blender_rock')
return obj
\ No newline at end of file
diff --git a/infinigen/assets/rocks/boulder.py b/infinigen/assets/rocks/boulder.py
index 3160b6c70..d05d585a4 100644
--- a/infinigen/assets/rocks/boulder.py
+++ b/infinigen/assets/rocks/boulder.py
@@ -14,16 +14,17 @@
from numpy.random import uniform
import gin
+from infinigen.assets.scatters import ivy
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
from infinigen.core import surface
from infinigen.assets.utils.object import trimesh2obj
from infinigen.assets.utils.decorate import geo_extension, write_attribute
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.core.util.random import log_uniform
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.placement.detail import remesh_with_attrs
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.core.util.blender import deep_clone_obj
from infinigen.core.placement.split_in_view import split_inview
@@ -143,5 +144,7 @@ def create_asset(self, i, placeholder, face_size=0.01, distance=0, **params):
with butil.DisableModifiers(skin_obj):
detail.adapt_mesh_resolution(skin_obj, face_size, method=self.adapt_mesh_method, apply=True)
+ butil.apply_modifiers(skin_obj)
tag_object(skin_obj, 'boulder')
+
return skin_obj
\ No newline at end of file
diff --git a/infinigen/assets/rocks/glowing_rocks.py b/infinigen/assets/rocks/glowing_rocks.py
index d8011b7ce..a52dbfe21 100644
--- a/infinigen/assets/rocks/glowing_rocks.py
+++ b/infinigen/assets/rocks/glowing_rocks.py
@@ -14,7 +14,7 @@
from infinigen.assets.rocks.blender_rock import BlenderRockFactory
from infinigen.core import surface
from infinigen.core.util.color import color_category
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def shader_glowrock(nw: NodeWrangler, transparent_for_bounce=True):
object_info = nw.new_node(Nodes.ObjectInfo_Shader)
@@ -53,11 +53,7 @@ def create_placeholder(self, i, loc, rot):
def create_asset(self, *args, **kwargs) -> bpy.types.Object:
src_obj = np.random.choice(list(self.rock_collection.objects))
-
- new_obj = src_obj.copy()
- new_obj.data = src_obj.data
- new_obj.animation_data_clear()
- bpy.context.collection.objects.link(new_obj)
+ new_obj = butil.deep_clone_obj(src_obj)
new_obj.rotation_euler = np.random.uniform(-np.pi, np.pi, 3)
new_obj.scale = np.random.uniform(0.7, 1.5, 3) * 0.5
@@ -71,5 +67,8 @@ def create_asset(self, *args, **kwargs) -> bpy.types.Object:
point_light = bpy.context.selected_objects[0]
point_light.data.energy = round(np.random.uniform(*self.watt_power_range))
point_light.parent = new_obj
+
+ butil.apply_transform(new_obj)
tag_object(new_obj, 'glowing_rocks')
+
return new_obj
diff --git a/infinigen/assets/rocks/pile.py b/infinigen/assets/rocks/pile.py
index 5eaca464d..e6d37fd0b 100644
--- a/infinigen/assets/rocks/pile.py
+++ b/infinigen/assets/rocks/pile.py
@@ -14,10 +14,12 @@
from infinigen.core.placement.detail import remesh_with_attrs
from infinigen.core.placement.factory import AssetFactory
import infinigen.core.util.blender as butil
-from infinigen.assets.utils.decorate import join_objects, multi_res, toggle_hide
+from infinigen.assets.utils.decorate import multi_res
+from infinigen.assets.utils.misc import toggle_hide
+from infinigen.assets.utils.object import join_objects
from infinigen.assets.utils.draw import surface_from_func
from infinigen.core.util.blender import deep_clone_obj
-from infinigen.assets.utils.tag import tag_object
+from infinigen.core.tagging import tag_object
from infinigen.core.util.random import log_uniform
diff --git a/infinigen/assets/scatters/clothes.py b/infinigen/assets/scatters/clothes.py
new file mode 100644
index 000000000..123197206
--- /dev/null
+++ b/infinigen/assets/scatters/clothes.py
@@ -0,0 +1,78 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+
+from collections.abc import Iterable
+
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.creatures.util.cloth_sim import bake_cloth
+from infinigen.assets.utils.decorate import read_co, subsurf
+from infinigen.core.placement.factory import make_asset_collection
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+
+
+def cloth_sim(clothes, obj=None, end_frame=50, **kwargs):
+ with butil.ViewportMode(clothes, mode='OBJECT'), butil.SelectObjects(clothes), butil.Suppress():
+ bpy.ops.ptcache.free_bake_all()
+ if obj is None:
+ obj = []
+ for o in obj if isinstance(obj, Iterable) else [obj]:
+ butil.modify_mesh(o, 'COLLISION', apply=False)
+ o.collision.damping_factor = .9
+ o.collision.cloth_friction = 10.
+ o.collision.friction_factor = 1.
+ o.collision.stickiness = .9
+ frame = bpy.context.scene.frame_current
+ butil.select_none()
+ with butil.Suppress():
+ mod = bake_cloth(clothes, kwargs, frame_start=1, frame_end=end_frame)
+ bpy.context.scene.frame_set(end_frame)
+ butil.apply_modifiers(clothes, mod)
+ for o in obj if isinstance(obj, Iterable) else [obj]:
+ with butil.SelectObjects(o):
+ bpy.ops.object.modifier_remove(modifier=o.modifiers[-1].name)
+ bpy.context.scene.frame_set(frame)
+ with butil.Suppress():
+ bpy.ops.ptcache.free_bake_all()
+
+
+class ClothesCover:
+ def __init__(self, bbox=(.3, .7, .3, .7), factory_fn=None, width=None, size=None):
+ from infinigen.assets.clothes import blanket, pants, shirt
+ probs = np.array([2, 1, 1])
+ if factory_fn is None:
+ factory_fn = np.random.choice(
+ [blanket.BlanketFactory, shirt.ShirtFactory, pants.PantsFactory],
+ p=probs / probs.sum()
+ )
+ self.factory = factory_fn(np.random.randint(1e5))
+ if width is not None:
+ self.factory.width = width
+ if size is not None:
+ self.factory.size = size
+ self.col = make_asset_collection(self.factory, name='clothes', centered=True, n=3, verbose=False)
+ self.bbox = bbox
+ self.z_offset = .2
+
+ def apply(self, obj, selection=None, **kwargs):
+ for obj in obj if isinstance(obj, list) else [obj]:
+ x, y, z = read_co(obj).T
+ clothes = deep_clone_obj(np.random.choice(self.col.objects), keep_materials=True)
+ clothes.parent = obj
+ clothes.location = uniform(self.bbox[0], self.bbox[1]) * (np.max(x) - np.min(x)) + np.min(
+ x
+ ), uniform(self.bbox[2], self.bbox[3]) * (np.max(y) - np.min(y)) + np.min(y), np.max(
+ z
+ ) + self.z_offset - np.min(read_co(clothes)[:, -1])
+ clothes.rotation_euler[-1] = uniform(0, np.pi * 2)
+ cloth_sim(clothes, obj, mass=.05, tension_stiffness=2, distance_min=5e-3)
+ subsurf(clothes, 2)
+
+
+def apply(obj, selection=None, **kwargs):
+ ClothesCover().apply(obj, selection, **kwargs)
\ No newline at end of file
diff --git a/infinigen/assets/scatters/ground_twigs.py b/infinigen/assets/scatters/ground_twigs.py
index 2dee88b0b..825ea305a 100644
--- a/infinigen/assets/scatters/ground_twigs.py
+++ b/infinigen/assets/scatters/ground_twigs.py
@@ -21,15 +21,19 @@
from infinigen.assets.trees.generate import make_twig_collection, random_species
from .chopped_trees import approx_settle_transform
+from ..utils.misc import toggle_show, toggle_hide
+
def apply(obj, selection=None, n_leaf=0, n_twig=10, **kwargs):
(_, twig_params, leaf_params), _ = random_species(season='winter')
twigs = make_twig_collection(np.random.randint(1e5), twig_params, leaf_params,
n_leaf=n_leaf, n_twig=n_twig, leaf_types=None, trunk_surface=surface.registry('bark'))
-
+
+ toggle_show(twigs)
for o in twigs.objects:
approx_settle_transform(o, samples=40)
+ toggle_hide(twigs)
scatter_obj = scatter_instances(
base_obj=obj, collection=twigs,
diff --git a/infinigen/assets/scatters/ivy.py b/infinigen/assets/scatters/ivy.py
index 04a47ea55..9a7def7d0 100644
--- a/infinigen/assets/scatters/ivy.py
+++ b/infinigen/assets/scatters/ivy.py
@@ -12,7 +12,9 @@
from infinigen.assets.leaves.leaf_maple import LeafFactoryMaple
from infinigen.assets.trees.generate import random_season
-from infinigen.assets.utils.decorate import assign_material, fix_tree
+
+from infinigen.assets.utils.mesh import fix_tree
+from infinigen.assets.utils.misc import assign_material
from infinigen.assets.utils.nodegroup import geo_base_selection, geo_radius
from infinigen.assets.utils.shortest_path import geo_shortest_path
from infinigen.core.nodes.node_info import Nodes
@@ -22,7 +24,7 @@
from infinigen.core.surface import shaderfunc_to_material
from infinigen.assets.materials.simple_brownish import shader_simple_brown
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def geo_leaf(nw: NodeWrangler, leaves):
diff --git a/infinigen/assets/scatters/lichen.py b/infinigen/assets/scatters/lichen.py
index 1bd79605a..aff9f28ef 100644
--- a/infinigen/assets/scatters/lichen.py
+++ b/infinigen/assets/scatters/lichen.py
@@ -11,7 +11,7 @@
import numpy as np
from numpy.random import uniform, normal as N
-from infinigen.assets.utils.decorate import assign_material
+from infinigen.assets.utils.misc import assign_material
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
from infinigen.core.placement.factory import AssetFactory, make_asset_collection
from infinigen.core.placement.instance_scatter import scatter_instances
@@ -21,7 +21,7 @@
from infinigen.assets.utils.object import data2mesh
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.assets.debris import LichenFactory
@@ -34,8 +34,8 @@ def __init__(self):
def apply(self, obj, selection=None):
scatter_obj = scatter_instances(
- base_obj=obj, collection=self.col,
- density=5e3, min_spacing=.08,
+ base_obj=obj, collection=self.col,
+ density=5e3, min_spacing=.08,
scale=1, scale_rand=N(0.5, 0.07),
selection=selection
)
@@ -46,8 +46,8 @@ def apply(obj, selection=None):
fac = LichenFactory(np.random.randint(1e5))
col = make_asset_collection(fac, name='lichen', n=5)
scatter_obj = scatter_instances(
- base_obj=obj, collection=col,
- density=5e3, min_spacing=.08,
+ base_obj=obj, collection=col,
+ density=5e3, min_spacing=.08,
scale=1, scale_rand=N(0.5, 0.07),
selection=selection
)
diff --git a/infinigen/assets/scatters/mollusk.py b/infinigen/assets/scatters/mollusk.py
index 1e2542036..d806cbf37 100644
--- a/infinigen/assets/scatters/mollusk.py
+++ b/infinigen/assets/scatters/mollusk.py
@@ -7,9 +7,8 @@
import numpy as np
from infinigen.assets.mollusk import MolluskFactory
-from infinigen.assets.utils.misc import CountInstance
+from infinigen.assets.utils.misc import CountInstance, toggle_hide
from infinigen.core.placement.factory import AssetFactory, make_asset_collection
-from infinigen.assets.utils.decorate import toggle_hide
from infinigen.core.util import blender as butil
from infinigen.core.nodes import node_utils
from infinigen.core.placement.instance_scatter import scatter_instances
@@ -29,7 +28,7 @@ def scaling(nw):
scatter_obj = scatter_instances('mollusk',
base_obj=obj, collection=mollusk,
- density=density, scaling=scaling,
+ density=density, scaling=scaling,
min_spacing=scale, normal=(0,0,1),
selection=selection, taper_density=True)
diff --git a/infinigen/assets/scatters/moss.py b/infinigen/assets/scatters/moss.py
index 686675dcd..2e39692cf 100644
--- a/infinigen/assets/scatters/moss.py
+++ b/infinigen/assets/scatters/moss.py
@@ -7,7 +7,7 @@
from numpy.random import uniform as U
from infinigen.core.placement.instance_scatter import scatter_instances
-from infinigen.assets.utils.decorate import assign_material
+from infinigen.assets.utils.misc import assign_material
from infinigen.core.placement.factory import make_asset_collection
from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
from infinigen.core import surface
@@ -31,10 +31,10 @@ def instance_index(nw: NodeWrangler, n):
nw.new_node(Nodes.FloatToInt, [nw.scalar_multiply(nw.musgrave(10), 2 * n)]), n)
scatter_obj = scatter_instances(
- base_obj=obj, collection=self.col,
- density=2e4, min_spacing=.005,
+ base_obj=obj, collection=self.col,
+ density=2e4, min_spacing=.005,
scale=1, scale_rand=U(0.3, 0.7),
- selection=selection,
+ selection=selection,
instance_index=instance_index)
return scatter_obj
diff --git a/infinigen/assets/scatters/mushroom.py b/infinigen/assets/scatters/mushroom.py
index 80661cddb..8b03640c2 100644
--- a/infinigen/assets/scatters/mushroom.py
+++ b/infinigen/assets/scatters/mushroom.py
@@ -6,8 +6,8 @@
from collections.abc import Iterable
-import bmesh
import bpy
+import bmesh
import numpy as np
from mathutils import Matrix
from numpy.random import uniform
diff --git a/infinigen/assets/scatters/slime_mold.py b/infinigen/assets/scatters/slime_mold.py
index 5ae38a7eb..cafbf825a 100644
--- a/infinigen/assets/scatters/slime_mold.py
+++ b/infinigen/assets/scatters/slime_mold.py
@@ -8,20 +8,22 @@
import numpy as np
from numpy.random import uniform
-from infinigen.assets.utils.decorate import assign_material, treeify
+from infinigen.assets.utils.mesh import treeify
+from infinigen.assets.utils.misc import assign_material
from infinigen.assets.utils.nodegroup import geo_base_selection, geo_radius
from infinigen.assets.utils.shortest_path import geo_shortest_path
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
-from infinigen.assets.utils.misc import build_color_ramp
+from infinigen.core.util.color import hsv2rgba
from infinigen.core.surface import shaderfunc_to_material
from infinigen.core.util import blender as butil
+from infinigen.core.nodes.node_utils import build_color_ramp
def shader_mold(nw: NodeWrangler, base_hue):
- bright_color = *colorsys.hsv_to_rgb((base_hue + uniform(-.04, .04)) % 1, uniform(.8, 1.), .8), 1
- dark_color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .6), .2), 1
+ bright_color = hsv2rgba((base_hue + uniform(-.04, .04)) % 1, uniform(.8, 1.), .8)
+ dark_color = hsv2rgba(base_hue, uniform(.4, .6), .2)
color = build_color_ramp(nw, nw.musgrave(10), [.0, .3, .7, 1.],
[dark_color, dark_color, bright_color, bright_color])
diff --git a/infinigen/assets/scatters/snow_layer.py b/infinigen/assets/scatters/snow_layer.py
index c9efef17f..7f70018f2 100644
--- a/infinigen/assets/scatters/snow_layer.py
+++ b/infinigen/assets/scatters/snow_layer.py
@@ -14,7 +14,7 @@
from infinigen.core.util import blender as butil
from infinigen.core.nodes import node_utils
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class Snowlayer:
def __init__(self):
diff --git a/infinigen/assets/scatters/utils/wind.py b/infinigen/assets/scatters/utils/wind.py
index a8d3181fc..2dc550abb 100644
--- a/infinigen/assets/scatters/utils/wind.py
+++ b/infinigen/assets/scatters/utils/wind.py
@@ -1,3 +1,4 @@
+
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
@@ -59,4 +60,4 @@ def wind_rotation(nw, speed=1.0, direction=None, scale=1.0, strength=30):
return rotation
def wind(*args, **kwargs):
- return lambda nw: wind_rotation(nw, *args, **kwargs)
\ No newline at end of file
+ return lambda nw: wind_rotation(nw, *args, **kwargs)
diff --git a/infinigen/assets/seating/__init__.py b/infinigen/assets/seating/__init__.py
new file mode 100644
index 000000000..5abb955c1
--- /dev/null
+++ b/infinigen/assets/seating/__init__.py
@@ -0,0 +1,11 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+
+from .sofa import SofaFactory, ArmChairFactory
+from .bedframe import BedFrameFactory
+from .pillow import PillowFactory
+from .mattress import MattressFactory
+from .bed import BedFactory
+from .chairs import *
diff --git a/infinigen/assets/seating/bed.py b/infinigen/assets/seating/bed.py
new file mode 100644
index 000000000..7a437dcd1
--- /dev/null
+++ b/infinigen/assets/seating/bed.py
@@ -0,0 +1,186 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from functools import cached_property
+
+import bpy
+import numpy as np
+import trimesh
+from numpy.random import uniform
+
+from ..scatters import clothes
+from . import BedFrameFactory, MattressFactory, PillowFactory
+from ..scatters.clothes import ClothesCover
+from ..utils.decorate import decimate, read_co, subsurf
+from ..utils.object import obj2trimesh
+from ...core import surface
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import random_general as rg, log_uniform
+from ...core.util.blender import deep_clone_obj
+
+
+class BedFactory(BedFrameFactory):
+ mattress_types = 'weighted_choice', (1, 'coiled'), (3, 'wrapped')
+ sheet_types = 'weighted_choice', (4, 'quilt'), (4, 'comforter'), (4, 'box_comforter'), (1, 'none')
+
+ def __init__(self, factory_seed, coarse=False):
+ super(BedFactory, self).__init__(factory_seed, coarse)
+ self.sheet_type = rg(self.sheet_types)
+ self.sheet_folded = uniform() < .5
+ self.has_cover = uniform() < .5
+ self.clothes_scatter = ClothesCover((.3, .7, .3, .7)) if uniform() < 0.2 else surface.NoApply
+
+ @cached_property
+ def mattress_factory(self):
+ factory = MattressFactory(self.factory_seed, self.coarse)
+ factory.type = rg(self.mattress_types)
+ factory.width = self.width * uniform(.88, .96)
+ factory.size = self.size * uniform(.88, .96)
+ return factory
+
+ @cached_property
+ def quilt_factory(self):
+ from ..clothes.blanket import BlanketFactory
+ factory = BlanketFactory(self.factory_seed, self.coarse)
+ factory.width = self.mattress_factory.width * uniform(1.4, 1.6)
+ factory.size = self.mattress_factory.size * uniform(.9, 1.1)
+ return factory
+
+ @cached_property
+ def comforter_factory(self):
+ from ..clothes.blanket import ComforterFactory
+ factory = ComforterFactory(self.factory_seed, self.coarse)
+ factory.width = self.mattress_factory.width * uniform(1.4, 1.8)
+ factory.size = self.mattress_factory.size * uniform(.9, 1.2)
+ return factory
+
+ @cached_property
+ def box_comforter_factory(self):
+ from ..clothes.blanket import BoxComforterFactory
+ factory = BoxComforterFactory(self.factory_seed, self.coarse)
+ factory.width = self.mattress_factory.width * uniform(1.4, 1.8)
+ factory.size = self.mattress_factory.size * uniform(.9, 1.2)
+ return factory
+
+ @cached_property
+ def cover_factory(self):
+ from ..clothes.blanket import BlanketFactory
+ factory = BlanketFactory(self.factory_seed, self.coarse)
+ factory.width = self.mattress_factory.width * uniform(1.6, 1.8)
+ factory.size = self.mattress_factory.size * uniform(.3, .4)
+ return factory
+
+ @cached_property
+ def towel_factory(self):
+ from ..clothes import TowelFactory
+ return TowelFactory(self.factory_seed)
+
+ @cached_property
+ def cloth_scatter(self):
+ return ClothesCover((.3, .7, .3, .7)) if uniform() < 0.0 else surface.NoApply
+
+ @cached_property
+ def pillow_factory(self):
+ return PillowFactory(self.factory_seed, self.coarse)
+
+ def create_asset(self, i, **params) -> bpy.types.Object:
+ frame = super().create_asset(i=i, **params)
+
+ mattress = self.make_mattress(i)
+ sheet = self.make_sheet(i, mattress, frame)
+ cover = self.make_cover(i, sheet, mattress)
+ self.cloth_scatter.apply(sheet)
+
+ n_pillows = np.random.randint(2, 4)
+ if n_pillows > 0:
+ pillow = self.pillow_factory(i)
+ pillows = [pillow] + [deep_clone_obj(pillow) for _ in range(n_pillows - 1)]
+ else:
+ pillows = []
+ self.pillow_factory.finalize_assets(pillows)
+ points = np.stack(
+ [uniform(.1, .4, 10) * self.size,
+ uniform(-.3, .3, 10) * self.width,
+ np.full(10, 1)], -1
+ )
+ self.scatter(pillows, points, [sheet, mattress])
+
+ n_towels = np.random.randint(1, 2)
+ if n_towels > 0:
+ towel = self.towel_factory(i)
+ towels = [towel] + [deep_clone_obj(towel) for _ in range(n_towels - 1)]
+ else:
+ towels = []
+ self.towel_factory.finalize_assets(towels)
+ points = np.stack(
+ [uniform(.5, .8, 10) * self.size,
+ uniform(-.3, .3, 10) * self.width,
+ np.full(10, 1)], -1
+ )
+ self.scatter(towels, points, [sheet, mattress])
+
+ for _ in [mattress, sheet, cover] + pillows + towels:
+ _.parent = frame
+ butil.select_none()
+ return frame
+
+ def make_mattress(self, i):
+ mattress = self.mattress_factory(i=i)
+ mattress.location = self.size / 2, 0, self.mattress_factory.thickness / 2
+ mattress.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(mattress, True)
+ self.mattress_factory.finalize_assets(mattress)
+ return mattress
+
+ def make_sheet(self, i, mattress, obj):
+ match self.sheet_type:
+ case 'quilt':
+ factory = self.quilt_factory
+ pressure = 0
+ case 'comforter':
+ factory = self.comforter_factory
+ pressure = uniform(1., 1.5)
+ case _:
+ factory = self.box_comforter_factory
+ pressure = log_uniform(8, 15)
+ sheet = factory(i)
+ if self.sheet_folded:
+ factory.fold(sheet)
+ factory.finalize_assets(sheet)
+ z_sheet = mattress.location[-1] + np.max(read_co(mattress)[:, -1])
+ sheet.location = factory.size / 2 + uniform(0, .15), 0, z_sheet
+ sheet.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(sheet, True)
+ clothes.cloth_sim(
+ sheet, [mattress, obj], mass=.05, tension_stiffness=2, distance_min=5e-3, use_pressure=True,
+ uniform_pressure_force=pressure, use_self_collision=self.sheet_folded
+ )
+ subsurf(sheet, 2)
+ return sheet
+
+ def make_cover(self, i, sheet, mattress):
+ cover = self.cover_factory(i)
+ self.cover_factory.finalize_assets(cover)
+ z_sheet = sheet.location[-1] + np.max(read_co(sheet)[:, -1])
+ cover.location = self.size / 2 + uniform(0, .3), 0, z_sheet
+ cover.rotation_euler[-1] = np.pi / 2
+ butil.apply_transform(cover, True)
+ clothes.cloth_sim(
+ cover, [sheet, mattress], 80, mass=.05, tension_stiffness=2, distance_min=5e-3
+ )
+ subsurf(cover, 2)
+ return cover
+
+ def scatter(self, pillows, points, bases):
+ dir = np.array([[0, 0, -1]])
+ lengths = np.full(len(points), np.inf)
+ for b in bases:
+ lengths = np.minimum(
+ lengths, trimesh.proximity.longest_ray(obj2trimesh(b), points, np.repeat(dir, len(points), 0))
+ )
+ points += dir * lengths[:, np.newaxis]
+ for a, loc in zip(pillows, decimate(points, len(pillows))):
+ a.location = loc
+ a.location[-1] += .02 - np.min(read_co(a)[:, -1])
+ a.rotation_euler[-1] = uniform(0, np.pi)
diff --git a/infinigen/assets/seating/bedframe.py b/infinigen/assets/seating/bedframe.py
new file mode 100644
index 000000000..d7029a2b3
--- /dev/null
+++ b/infinigen/assets/seating/bedframe.py
@@ -0,0 +1,179 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.seating.chairs.chair import ChairFactory
+from infinigen.assets.seating.mattress import make_coiled
+from infinigen.assets.utils.decorate import (
+ subdivide_edge_ring, remove_faces, read_normal, read_co, write_co,
+ remove_vertices, select_faces, write_attribute,
+)
+from infinigen.assets.utils.object import new_grid, join_objects
+from infinigen.core import surface
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import random_general as rg
+from infinigen.assets.material_assignments import AssetList
+
+
+class BedFrameFactory(ChairFactory):
+ scale = 1.
+ leg_decor_types = 'weighted_choice', (2, 'coiled'), (2, 'pad'), (1, 'plain'), (2, 'legs')
+ back_types = 'weighted_choice', (3, 'coiled'), (3, 'pad'), (2, 'whole'), (1, 'horizontal-bar'), (1, 'vertical-bar')
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = log_uniform(1.4, 2.4)
+ self.size = uniform(2, 2.4)
+ self.thickness = uniform(.05, .12)
+ self.has_all_legs = uniform() < .2
+ self.leg_thickness = uniform(.08, .12)
+ self.leg_height = uniform(.2, .6)
+ self.leg_decor_type = rg(self.leg_decor_types)
+ self.leg_decor_wrapped = uniform() < .5
+ self.back_height = uniform(.5, 1.3)
+ self.seat_back = 1
+ self.seat_subdivisions_x = np.random.randint(1, 4)
+ self.seat_subdivisions_y = int(log_uniform(4, 10))
+ self.has_arm = False
+ self.leg_type = 'vertical'
+ self.leg_x_offset = 0
+ self.leg_y_offset = 0, 0
+ self.back_x_offset = 0
+ self.back_y_offset = 0
+
+ materials = AssetList['BedFrameFactory']()
+ self.surface = materials['surface'].assign_material()
+ self.limb_surface = materials['limb_surface'].assign_material()
+
+ scratch_prob, edge_wear_prob = materials['wear_tear_prob']
+ self.scratch, self.edge_wear = materials['wear_tear']
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.clothes_scatter = surface.NoApply
+ self.dot_distance = log_uniform(.16, .2)
+ self.dot_size = uniform(.005, .02)
+ self.dot_depth = uniform(.04, .08)
+ self.panel_distance = uniform(.3, .5)
+ self.panel_margin = uniform(.01, .02)
+ self.post_init()
+
+ def make_seat(self):
+ obj = new_grid(x_subdivisions=self.seat_subdivisions_x, y_subdivisions=self.seat_subdivisions_y)
+ obj.scale = (self.width - self.leg_thickness) / 2, (self.size - self.leg_thickness) / 2, 1
+ butil.apply_transform(obj, True)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.delete(type='ONLY_FACE')
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, 0, self.thickness)})
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.leg_thickness - 1e-3, offset=0, solidify_mode='NON_MANIFOLD')
+ obj.location = 0, -self.size / 2, -self.thickness / 2
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'BEVEL', width=self.bevel_width, segments=8)
+ return obj
+
+ def make_legs(self):
+ legs = super().make_legs()
+ if self.has_all_legs:
+ leg_starts = np.array(
+ [[-1, -.5, 0], [0, -1, 0], [0, 0, 0], [1, -.5, 0]]
+ ) * np.array(
+ [[self.width / 2, self.size, 0]]
+ )
+ leg_ends = leg_starts.copy()
+ leg_ends[0, 0] -= self.leg_x_offset
+ leg_ends[3, 0] += self.leg_x_offset
+ leg_ends[2, 1] += self.leg_y_offset[0]
+ leg_ends[1, 1] -= self.leg_y_offset[1]
+ leg_ends[:, -1] = -self.leg_height
+ legs += self.make_limb(leg_ends, leg_starts)
+ return legs
+
+ def make_leg_decors(self, legs):
+ if self.leg_decor_type == 'none':
+ return super().make_leg_decors(legs)
+ obj = join_objects([deep_clone_obj(_) for _ in legs])
+ x, y, z = read_co(obj).T
+ z = np.maximum(z, -self.leg_height * uniform(.7, .9))
+ write_co(obj, np.stack([x, y, z], -1))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.convex_hull()
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ remove_faces(obj, np.abs(read_normal(obj)[:, -1]) > .5)
+ if self.leg_decor_wrapped:
+ x, y, z = read_co(obj).T
+ x[x < 0] -= self.leg_thickness / 2 + 1e-3
+ x[x > 0] += self.leg_thickness / 2 + 1e-3
+ y[y < -self.size / 2] -= self.leg_thickness / 2 + 1e-3
+ y[y > -self.size / 2] += self.leg_thickness / 2 + 1e-3
+ write_co(obj, np.stack([x, y, z], -1))
+ match self.leg_decor_type:
+ case 'coiled':
+ self.divide(obj, self.dot_distance)
+ make_coiled(obj, self.dot_distance, self.dot_depth, self.dot_size)
+ case 'pad':
+ self.divide(obj, self.panel_distance)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.inset(thickness=self.panel_margin, depth=self.panel_margin, use_individual=True)
+ butil.modify_mesh(obj, 'BEVEL', segments=4)
+ write_attribute(obj, 1, 'panel', 'FACE')
+ return [obj]
+
+ def divide(self, obj, distance):
+ for i, size in enumerate(obj.dimensions):
+ axis = np.zeros(3)
+ axis[i] = 1
+ distance = distance if i != 2 else distance * uniform(.5, 1.)
+ subdivide_edge_ring(obj, int(np.ceil(size / distance)), axis)
+
+ def make_back_decors(self, backs, finalize=True):
+ decors = super().make_back_decors(backs)
+ match self.back_type:
+ case 'coiled':
+ obj = self.make_back(backs)
+ self.divide(obj, self.dot_distance)
+ make_coiled(obj, self.dot_distance, self.dot_depth, self.dot_size)
+ obj.scale = (1 - 1e-3,) * 3
+ write_attribute(obj, 1, 'panel', 'FACE')
+ with butil.ViewportMode(decors[0], 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bisect(plane_co=(0, 0, self.back_height), plane_no=(0, 0, 1), clear_inner=True)
+ return [obj] + decors
+ case 'pad':
+ obj = self.make_back(backs)
+ self.divide(obj, self.panel_distance)
+ with butil.ViewportMode(obj, 'EDIT'):
+ select_faces(obj, np.abs(read_normal(obj)[:, 1]) > .5)
+ bpy.ops.mesh.inset(thickness=self.panel_margin, depth=self.panel_margin, use_individual=True)
+ butil.modify_mesh(obj, 'BEVEL', segments=4)
+ write_attribute(obj, 1, 'panel', 'FACE')
+ obj.scale = (1 - 1e-3,) * 3
+ with butil.ViewportMode(decors[0], 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bisect(plane_co=(0, 0, self.back_height), plane_no=(0, 0, 1), clear_inner=True)
+ return [obj] + decors
+ case _:
+ return decors
+
+ def make_back(self, backs):
+ obj = join_objects([deep_clone_obj(b) for b in backs])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.convex_hull()
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=np.minimum(self.thickness, self.leg_thickness), offset=0)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ return obj
diff --git a/infinigen/assets/seating/chairs/__init__.py b/infinigen/assets/seating/chairs/__init__.py
new file mode 100644
index 000000000..5d10c9c3b
--- /dev/null
+++ b/infinigen/assets/seating/chairs/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .bar_chair import BarChairFactory
+from .office_chair import OfficeChairFactory
+from .chair import ChairFactory
diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py
new file mode 100644
index 000000000..733cb79ac
--- /dev/null
+++ b/infinigen/assets/seating/chairs/bar_chair.py
@@ -0,0 +1,173 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+from numpy.random import uniform, choice
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core import surface
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core import tagging, tags as t
+
+from infinigen.assets.seating.chairs.seats.round_seats import generate_round_seats
+
+from infinigen.assets.tables.cocktail_table import geometry_create_legs
+from infinigen.assets.material_assignments import AssetList
+
+def geometry_assemble_chair(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ generateseat = nw.new_node(generate_round_seats(thickness=kwargs['Top Thickness'],
+ radius=kwargs['Top Profile Width'],
+ seat_material=kwargs['SeatMaterial']).name)
+
+ seat_instance = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': generateseat,
+ 'Translation': (0.0000, 0.0000, kwargs['Top Height'])})
+
+ legs = nw.new_node(geometry_create_legs(**kwargs).name)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [seat_instance, legs]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+class BarChairFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(BarChairFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params, leg_style = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params(leg_style)
+
+ self.params.update(self.material_params)
+
+ def get_material_params(self, leg_style):
+ material_assignments = AssetList['BarChairFactory'](leg_style=leg_style)
+
+ params = {
+ "SeatMaterial": material_assignments['seat'].assign_material(),
+ "LegMaterial": material_assignments['leg'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # all in meters
+ if dimensions is None:
+ x = uniform(0.35, 0.45)
+ z = uniform(0.7, 1)
+ dimensions = (x, x, z)
+
+ x, y, z = dimensions
+
+ top_thickness = uniform(0.06, 0.10)
+
+ leg_style = choice(['straight', 'single_stand', 'wheeled'])
+
+ parameters = {
+ 'Top Profile Width': x,
+ 'Top Thickness': top_thickness,
+ 'Height': z,
+ 'Top Height': z - top_thickness,
+ 'Leg Style': leg_style,
+ 'Leg NGon': choice([4, 32]),
+ 'Leg Placement Top Relative Scale': 0.7,
+ 'Leg Placement Bottom Relative Scale': uniform(1.1, 1.3),
+ 'Leg Height': 1.0,
+ }
+
+ if leg_style == "single_stand":
+ leg_number = 1
+ leg_diameter = uniform(0.7*x, 0.9*x)
+
+ leg_curve_ctrl_pts = [(0.0, uniform(0.1, 0.2)),
+ (0.5, uniform(0.1, 0.2)), (0.9, uniform(0.2, 0.3)), (1.0, 1.0)]
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood'])
+ })
+
+ elif leg_style == "straight":
+ leg_diameter = uniform(0.04, 0.06)
+ leg_number = choice([3, 4])
+
+ leg_curve_ctrl_pts = [(0.0, 1.0), (0.4, uniform(0.85, 0.95)), (1.0, uniform(0.4, 0.6))]
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood']),
+ 'Strecher Relative Pos': uniform(0.6, 0.9),
+ 'Strecher Increament': choice([0, 1, 2])
+ })
+
+ elif leg_style == "wheeled":
+ leg_diameter = uniform(0.03, 0.05)
+ leg_number = 1
+ pole_number = choice([4, 5])
+ joint_height = uniform(0.5, 0.8) * (z - top_thickness)
+ wheel_arc_sweep_angle = uniform(120, 240)
+ wheel_width = uniform(0.11, 0.15)
+ wheel_rot = uniform(0, 360)
+ pole_length = uniform(1.6, 2.0)
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Pole Number': pole_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Joint Height': joint_height,
+ 'Leg Wheel Arc Sweep Angle': wheel_arc_sweep_angle,
+ 'Leg Wheel Width': wheel_width,
+ 'Leg Wheel Rot': wheel_rot,
+ 'Leg Pole Length': pole_length,
+ # 'Leg Material': choice(['metal'])
+ })
+
+ else:
+ raise NotImplementedError
+
+
+
+ return parameters, leg_style
+
+ def create_asset(self, **params):
+
+ bpy.ops.mesh.primitive_plane_add(
+ size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ surface.add_geomod(obj, geometry_assemble_chair, apply=True, input_kwargs=self.params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
diff --git a/infinigen/assets/seating/chairs/chair.py b/infinigen/assets/seating/chairs/chair.py
new file mode 100644
index 000000000..ed37dafdf
--- /dev/null
+++ b/infinigen/assets/seating/chairs/chair.py
@@ -0,0 +1,364 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import (
+ read_co, read_edge_center, read_edge_direction, remove_edges,
+ remove_vertices, select_edges, solidify, subsurf, write_attribute, write_co,
+)
+from infinigen.assets.utils.draw import align_bezier, bezier_curve
+from infinigen.assets.utils.nodegroup import geo_radius
+from infinigen.assets.utils.object import join_objects, new_bbox
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import NoApply
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed, normalize
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+from infinigen.core.util.random import random_general as rg
+
+
+class ChairFactory(AssetFactory):
+ back_types = 'weighted_choice', (1, 'whole'), (1, 'partial'), (1, 'horizontal-bar'), (1, 'vertical-bar')
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = uniform(.4, .5)
+ self.size = uniform(.38, .45)
+ self.thickness = uniform(.04, .08)
+ self.bevel_width = self.thickness * (.1 if uniform() < .4 else .5)
+ self.seat_back = uniform(.7, 1.) if uniform() < .75 else 1.
+ self.seat_mid = uniform(.7, .8)
+ self.seat_mid_x = uniform(self.seat_back + self.seat_mid * (1 - self.seat_back), 1)
+ self.seat_mid_z = uniform(0, .5)
+ self.seat_front = uniform(1., 1.2)
+ self.is_seat_round = uniform() < .6
+ self.is_seat_subsurf = uniform() < .5
+
+ self.leg_thickness = uniform(.04, .06)
+ self.limb_profile = uniform(1.5, 2.5)
+ self.leg_height = uniform(.45, .5)
+ self.back_height = uniform(.4, .5)
+ self.is_leg_round = uniform() < .5
+ self.leg_type = np.random.choice(['vertical', 'straight', 'up-curved', 'down-curved'])
+
+ self.leg_x_offset = 0
+ self.leg_y_offset = 0, 0
+ self.back_x_offset = 0
+ self.back_y_offset = 0
+
+ self.has_leg_x_bar = uniform() < .6
+ self.has_leg_y_bar = uniform() < .6
+ self.leg_offset_bar = uniform(.2, .4), uniform(.6, .8)
+
+ self.has_arm = uniform() < 0.7
+ self.arm_thickness = uniform(.04, .06)
+ self.arm_height = self.arm_thickness * uniform(.6, 1)
+ self.arm_y = uniform(.8, 1) * self.size
+ self.arm_z = uniform(.3, .6) * self.back_height
+ self.arm_mid = np.array([uniform(-.03, .03), uniform(-.03, .09), uniform(-.09, .03)])
+ self.arm_profile = log_uniform(.1, 3, 2)
+
+ self.back_thickness = uniform(.04, .05)
+ self.back_type = rg(self.back_types)
+ self.back_profile = [(0, 1)]
+ self.back_vertical_cuts = np.random.randint(1, 4)
+ self.back_partial_scale = uniform(1, 1.4)
+
+ materials = AssetList['ChairFactory']()
+ self.limb_surface = materials['limb'].assign_material()
+ self.surface = materials['surface'].assign_material()
+ if uniform() < .3:
+ self.panel_surface = self.surface
+ else:
+ self.panel_surface = materials['panel'].assign_material()
+
+ scratch_prob, edge_wear_prob = materials['wear_tear_prob']
+ self.scratch, self.edge_wear = materials['wear_tear']
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ self.scratch = None
+ if not is_edge_wear:
+ self.edge_wear = None
+
+ #from infinigen.assets.clothes import blanket
+ #from infinigen.assets.scatters.clothes import ClothesCover
+ #self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(.8, 1.2),
+ # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply()
+ self.clothes_scatter = NoApply()
+ self.post_init()
+
+ def post_init(self):
+ with FixedSeed(self.factory_seed):
+ if self.leg_type == 'vertical':
+ self.leg_x_offset = 0
+ self.leg_y_offset = 0, 0
+ self.back_x_offset = 0
+ self.back_y_offset = 0
+ else:
+ self.leg_x_offset = self.width * uniform(.05, .2)
+ self.leg_y_offset = self.size * uniform(.05, .2, 2)
+ self.back_x_offset = self.width * uniform(-.1, .15)
+ self.back_y_offset = self.size * uniform(.1, .25)
+
+ match self.back_type:
+ case 'partial':
+ self.back_profile = (uniform(.4, .8), 1),
+ case 'horizontal-bar':
+ n_cuts = np.random.randint(2, 4)
+ locs = uniform(1, 2, n_cuts).cumsum()
+ locs = locs / locs[-1]
+ ratio = uniform(.5, .75)
+ locs = np.array([(p + ratio * (l - p), l) for p, l in zip([0, *locs[:-1]], locs)])
+ lowest = uniform(0, .4)
+ self.back_profile = locs * (1 - lowest) + lowest
+ case 'vertical-bar':
+ self.back_profile = (uniform(.8, .9), 1),
+ case _:
+ self.back_profile = [(0, 1)]
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ obj = new_bbox(
+ -self.width / 2 - max(self.leg_x_offset, self.back_x_offset),
+ self.width / 2 + max(self.leg_x_offset, self.back_x_offset),
+ -self.size - self.leg_y_offset[1] - self.leg_thickness * .5,
+ max(self.leg_y_offset[0], self.back_y_offset),
+ -self.leg_height,
+ self.back_height * 1.2
+ )
+ obj.rotation_euler.z += np.pi / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def create_asset(self, **params) -> bpy.types.Object:
+
+ obj = self.make_seat()
+ legs = self.make_legs()
+ backs = self.make_backs()
+
+ parts = [obj] + legs + backs
+ parts.extend(self.make_leg_decors(legs))
+ if self.has_arm:
+ parts.extend(self.make_arms(obj, backs))
+ parts.extend(self.make_back_decors(backs))
+
+ for obj in legs:
+ self.solidify(obj, 2)
+ for obj in backs:
+ self.solidify(obj, 2, self.back_thickness)
+
+ obj = join_objects(parts)
+ obj.rotation_euler.z += np.pi / 2
+ butil.apply_transform(obj)
+
+ with FixedSeed(self.factory_seed):
+ # TODO: wasteful to create unique materials for each individual asset
+ self.surface.apply(obj)
+ self.panel_surface.apply(obj, selection='panel')
+ self.limb_surface.apply(obj, selection='limb')
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+ def make_seat(self):
+ x_anchors = np.array(
+ [0, -self.seat_back, -self.seat_mid_x, -1, 0, 1, self.seat_mid_x, self.seat_back,
+ 0]
+ ) * self.width / 2
+ y_anchors = np.array([0, 0, -self.seat_mid, -1, -self.seat_front, -1, -self.seat_mid, 0, 0]) * self.size
+ z_anchors = np.array([0, 0, self.seat_mid_z, 0, 0, 0, self.seat_mid_z, 0, 0]) * self.thickness
+ vector_locations = [1, 7] if self.is_seat_round else [1, 3, 5, 7]
+ obj = bezier_curve((x_anchors, y_anchors, z_anchors), vector_locations, 8)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.fill_grid(use_interp_simple=True)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=0)
+ subsurf(obj, 1, not self.is_seat_subsurf)
+ butil.modify_mesh(obj, 'BEVEL', width=self.bevel_width, segments=8)
+ return obj
+
+ def make_legs(self):
+ leg_starts = np.array(
+ [[-self.seat_back, 0, 0], [-1, -1, 0], [1, -1, 0], [self.seat_back, 0, 0]]
+ ) * np.array(
+ [[self.width / 2, self.size, 0]]
+ )
+ leg_ends = leg_starts.copy()
+ leg_ends[[0, 1], 0] -= self.leg_x_offset
+ leg_ends[[2, 3], 0] += self.leg_x_offset
+ leg_ends[[0, 3], 1] += self.leg_y_offset[0]
+ leg_ends[[1, 2], 1] -= self.leg_y_offset[1]
+ leg_ends[:, -1] = -self.leg_height
+ return self.make_limb(leg_ends, leg_starts)
+
+ def make_limb(self, leg_ends, leg_starts):
+ limbs = []
+ for leg_start, leg_end in zip(leg_starts, leg_ends):
+ match self.leg_type:
+ case 'up-curved':
+ axes = [(0, 0, 1), None]
+ scale = [self.limb_profile, 1]
+ case 'down-curved':
+ axes = [None, (0, 0, 1)]
+ scale = [1, self.limb_profile]
+ case _:
+ axes = None
+ scale = None
+ limb = align_bezier(np.stack([leg_start, leg_end], -1), axes, scale, resolution=64)
+ limb.location = np.array(
+ [1 if leg_start[0] < 0 else -1, 1 if leg_start[1] < -self.size / 2 else -1,
+ 0]
+ ) * self.leg_thickness / 2
+ butil.apply_transform(limb, True)
+ limbs.append(limb)
+ return limbs
+
+ def make_backs(self):
+ back_starts = np.array([[-self.seat_back, 0, 0], [self.seat_back, 0, 0]]) * self.width / 2
+ back_ends = back_starts.copy()
+ back_ends[:, 0] += np.array([self.back_x_offset, -self.back_x_offset])
+ back_ends[:, 1] = self.back_y_offset
+ back_ends[:, 2] = self.back_height
+ return self.make_limb(back_starts, back_ends)
+
+ def make_leg_decors(self, legs):
+ decors = []
+ if self.has_leg_x_bar:
+ z_height = -self.leg_height * uniform(*self.leg_offset_bar)
+ locs = []
+ for leg in legs:
+ co = read_co(leg)
+ locs.append(co[np.argmin(np.abs(co[:, -1] - z_height))])
+ decors.append(self.solidify(bezier_curve(np.stack([locs[0], locs[3]], -1)), 0))
+ decors.append(self.solidify(bezier_curve(np.stack([locs[1], locs[2]], -1)), 0))
+ if self.has_leg_y_bar:
+ z_height = -self.leg_height * uniform(*self.leg_offset_bar)
+ locs = []
+ for leg in legs:
+ co = read_co(leg)
+ locs.append(co[np.argmin(np.abs(co[:, -1] - z_height))])
+ decors.append(self.solidify(bezier_curve(np.stack([locs[0], locs[1]], -1)), 1))
+ decors.append(self.solidify(bezier_curve(np.stack([locs[2], locs[3]], -1)), 1))
+ for d in decors:
+ write_attribute(d, 1, 'limb', 'FACE')
+ return decors
+
+ def make_back_decors(self, backs, finalize=True):
+ obj = join_objects([deep_clone_obj(b) for b in backs])
+ x, y, z = read_co(obj).T
+ x += np.where(x > 0, self.back_thickness / 2, -self.back_thickness / 2)
+ write_co(obj, np.stack([x, y, z], -1))
+ smoothness = uniform(0, 1)
+ profile_shape_factor = uniform(0, .4)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ center = read_edge_center(obj)
+ for z_min, z_max in self.back_profile:
+ select_edges(
+ obj, (z_min * self.back_height <= center[:, -1]) & (
+ center[:, -1] <= z_max * self.back_height)
+ )
+ bpy.ops.mesh.bridge_edge_loops(
+ number_cuts=32, interpolation='LINEAR', smoothness=smoothness,
+ profile_shape_factor=profile_shape_factor
+ )
+ bpy.ops.mesh.select_loose()
+ bpy.ops.mesh.delete()
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=np.minimum(self.thickness, self.back_thickness), offset=0)
+ if finalize:
+ butil.modify_mesh(obj, 'BEVEL', width=self.bevel_width, segments=8)
+ parts = [obj]
+ if self.back_type == 'vertical-bar':
+ other = join_objects([deep_clone_obj(b) for b in backs])
+ with butil.ViewportMode(other, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bridge_edge_loops(
+ number_cuts=self.back_vertical_cuts, interpolation='LINEAR',
+ smoothness=smoothness, profile_shape_factor=profile_shape_factor
+ )
+ bpy.ops.mesh.select_all(action='INVERT')
+ bpy.ops.mesh.delete()
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.delete(type='ONLY_FACE')
+ remove_edges(other, np.abs(read_edge_direction(other)[:, -1]) < .5)
+ remove_vertices(other, lambda x, y, z: z < -self.thickness / 2)
+ remove_vertices(
+ other, lambda x, y, z: z > (
+ self.back_profile[0][0] + self.back_profile[0][1]) * self.back_height / 2
+ )
+ parts.append(self.solidify(other, 2, self.back_thickness))
+ elif self.back_type == 'partial':
+ co = read_co(obj)
+ co[:, 1] *= self.back_partial_scale
+ write_co(obj, co)
+ for p in parts:
+ write_attribute(p, 1, 'panel', 'FACE')
+ return parts
+
+ def make_arms(self, base, backs):
+ co = read_co(base)
+ end = co[np.argmin(co[:, 0] - (np.abs(co[:, 1] + self.arm_y) < .02))]
+ end[0] += self.arm_thickness / 4
+ end_ = end.copy()
+ end_[0] = -end[0]
+ arms = []
+ co = read_co(backs[0])
+ start = co[np.argmin(co[:, 0] - (np.abs(co[:, -1] - self.arm_z) < .02))]
+ start[0] -= self.arm_thickness / 4
+ start_ = start.copy()
+ start_[0] = -start[0]
+ for start, end in zip([start, start_], [end, end_]):
+ mid = np.array(
+ [end[0] + self.arm_mid[0] * (-1 if end[0] > 0 else 1), end[1] + self.arm_mid[1],
+ start[2] + self.arm_mid[2]]
+ )
+ arm = align_bezier(
+ np.stack([start, mid, end], -1),
+ np.array([[end[0] - start[0], end[1] - start[1], 0], [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], [0, 0, 1]]),
+ [1, *self.arm_profile, 1]
+ )
+ if self.is_leg_round:
+ surface.add_geomod(
+ arm, geo_radius, apply=True, input_args=[self.arm_thickness / 2, 32],
+ input_kwargs={'to_align_tilt': False}
+ )
+ else:
+ with butil.ViewportMode(arm, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={
+ 'value': (self.arm_thickness if end[0] < 0 else -self.arm_thickness, 0, 0)
+ }
+ )
+ butil.modify_mesh(arm, 'SOLIDIFY', thickness=self.arm_height, offset=0)
+ write_attribute(arm, 1, 'limb', 'FACE')
+ arms.append(arm)
+ return arms
+
+ def solidify(self, obj, axis, thickness=None):
+ if thickness is None:
+ thickness = self.leg_thickness
+ if self.is_leg_round:
+ solidify(obj, axis, thickness)
+ butil.modify_mesh(obj, 'BEVEL', width=self.bevel_width, segments=8)
+ else:
+ surface.add_geomod(obj, geo_radius, apply=True, input_args=[thickness / 2, 32])
+ write_attribute(obj, 1, 'limb', 'FACE')
+ return obj
+
+
diff --git a/infinigen/assets/seating/chairs/office_chair.py b/infinigen/assets/seating/chairs/office_chair.py
new file mode 100644
index 000000000..7a4f3ba5a
--- /dev/null
+++ b/infinigen/assets/seating/chairs/office_chair.py
@@ -0,0 +1,206 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+import bpy
+
+import numpy as np
+from numpy.random import uniform, choice
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface, tagging
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.seating.chairs.seats.curvy_seats import generate_curvy_seats
+
+from infinigen.assets.tables.cocktail_table import geometry_create_legs
+from infinigen.assets.material_assignments import AssetList
+
+def geometry_assemble_chair(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ generateseat = nw.new_node(generate_curvy_seats().name,
+ input_kwargs={
+ 'Width': kwargs['Top Profile Width'],
+ 'Front Relative Width': kwargs['Top Front Relative Width'],
+ 'Front Bent': kwargs['Top Front Bent'],
+ 'Seat Bent': kwargs['Top Seat Bent'],
+ 'Mid Bent': kwargs['Top Mid Bent'],
+ 'Mid Relative Width': kwargs['Top Mid Relative Width'],
+ 'Back Bent': kwargs['Top Back Bent'],
+ 'Back Relative Width': kwargs['Top Back Relative Width'],
+ 'Mid Pos': kwargs['Top Mid Pos'],
+ 'Seat Height': kwargs['Top Thickness'],
+ })
+
+ seat_instance = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': generateseat,
+ 'Translation': (0.0000, 0.0000, kwargs['Top Height'])})
+
+ seat_instance = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': seat_instance, 'Material': kwargs['TopMaterial']})
+
+ legs = nw.new_node(geometry_create_legs(**kwargs).name)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [seat_instance, legs]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+class OfficeChairFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(OfficeChairFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params, leg_style = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params(leg_style)
+ self.params.update(self.material_params)
+
+ def get_material_params(self, leg_style):
+ material_assignments = AssetList['OfficeChairFactory'](leg_style)
+ params = {
+ "TopMaterial": material_assignments['top'].assign_material(),
+ "LegMaterial": material_assignments['leg'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # all in meters
+
+ if dimensions is None:
+ x = uniform(0.5, 0.6)
+ z = uniform(1.0, 1.4)
+ dimensions = (
+ x, x, z
+ )
+
+ x, y, z = dimensions
+
+ top_thickness = uniform(0.5, 0.7)
+
+ # straight has the bug that seat and legs are disjoint, so disable for now.
+
+ # leg_style = choice(['straight', 'single_stand', 'wheeled'])
+ leg_style = choice(['single_stand', 'wheeled'])
+
+ parameters = {
+ 'Top Profile Width': x,
+ 'Top Thickness': top_thickness,
+ 'Top Front Relative Width': uniform(0.5, 0.8),
+ 'Top Front Bent': uniform(-1.5, -0.4),
+ 'Top Seat Bent': uniform(-1.5, -0.4),
+ 'Top Mid Bent': uniform(-2.4, -0.5),
+ 'Top Mid Relative Width': uniform(0.5, 0.9),
+ 'Top Back Bent': uniform(-1, -0.1),
+ 'Top Back Relative Width': uniform(0.6, 0.9),
+ 'Top Mid Pos': uniform(0.4, 0.6),
+ # 'Top Material': choice(['leather', 'wood', 'plastic', 'glass']),
+ 'Height': z,
+ 'Top Height': z - top_thickness,
+ 'Leg Style': leg_style,
+ 'Leg NGon': choice([4, 32]),
+ 'Leg Placement Top Relative Scale': 0.7,
+ 'Leg Placement Bottom Relative Scale': uniform(1.1, 1.3),
+ 'Leg Height': 1.0,
+ }
+
+ if leg_style == "single_stand":
+ leg_number = 1
+ leg_diameter = uniform(0.7*x, 0.9*x)
+
+ leg_curve_ctrl_pts = [(0.0, uniform(0.1, 0.2)),
+ (0.5, uniform(0.1, 0.2)), (0.9, uniform(0.2, 0.3)), (1.0, 1.0)]
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood'])
+ })
+
+ elif leg_style == "straight":
+ leg_diameter = uniform(0.04, 0.06)
+ leg_number = 4
+
+ leg_curve_ctrl_pts = [(0.0, 1.0), (0.4, uniform(0.85, 0.95)), (1.0, uniform(0.4, 0.6))]
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood']),
+ 'Strecher Relative Pos': uniform(0.2, 0.6),
+ 'Strecher Increament': choice([0, 1, 2])
+ })
+
+ elif leg_style == "wheeled":
+ leg_diameter = uniform(0.03, 0.05)
+ leg_number = 1
+ pole_number = choice([4, 5])
+ joint_height = uniform(0.5, 0.8) * (z - top_thickness)
+ wheel_arc_sweep_angle = uniform(120, 240)
+ wheel_width = uniform(0.11, 0.15)
+ wheel_rot = uniform(0, 360)
+ pole_length = uniform(1.6, 2.0)
+
+ parameters.update({
+ 'Leg Number': leg_number,
+ 'Leg Pole Number': pole_number,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Joint Height': joint_height,
+ 'Leg Wheel Arc Sweep Angle': wheel_arc_sweep_angle,
+ 'Leg Wheel Width': wheel_width,
+ 'Leg Wheel Rot': wheel_rot,
+ 'Leg Pole Length': pole_length,
+ # 'Leg Material': choice(['metal'])
+ })
+
+ else:
+ raise NotImplementedError
+
+ return parameters, leg_style
+
+ def create_asset(self, **params):
+
+ bpy.ops.mesh.primitive_plane_add(
+ size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+
+ surface.add_geomod(obj, geometry_assemble_chair, apply=True, input_kwargs=self.params)
+ tagging.tag_system.relabel_obj(obj)
+
+ obj.rotation_euler.z += np.pi / 2
+ butil.apply_transform(obj)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
diff --git a/infinigen/assets/seating/chairs/seats/curvy_seats.py b/infinigen/assets/seating/chairs/seats/curvy_seats.py
new file mode 100644
index 000000000..128d5d1ff
--- /dev/null
+++ b/infinigen/assets/seating/chairs/seats/curvy_seats.py
@@ -0,0 +1,150 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_utils import nodegroup_bent
+from infinigen.assets.table_decorations.utils import nodegroup_lofting, nodegroup_warp_around_curve
+
+# TODO: set material automatically
+
+@node_utils.to_nodegroup('generate_curvy_seats', singleton=False, type='GeometryNodeTree')
+def generate_curvy_seats(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'U Resolution', 256),
+ ('NodeSocketInt', 'V Resolution', 128),
+ ('NodeSocketFloat', 'Width', 0.5000),
+ ('NodeSocketFloat', 'Thickness', 0.0300),
+ ('NodeSocketFloat', 'Front Relative Width', 0.5000),
+ ('NodeSocketFloat', 'Front Bent', -0.3800),
+ ('NodeSocketFloat', 'Seat Bent', -0.5600),
+ ('NodeSocketFloat', 'Mid Relative Width', 0.5000),
+ ('NodeSocketFloat', 'Mid Bent', -0.7000),
+ ('NodeSocketFloat', 'Back Relative Width', 0.5000),
+ ('NodeSocketFloat', 'Back Bent', -0.2000),
+ ('NodeSocketFloat', 'Top Relative Width', 0.5000),
+ ('NodeSocketFloat', 'Top Bent', -0.2000),
+ ('NodeSocketFloat', 'Seat Height', 0.6000),
+ ('NodeSocketFloat', 'Mid Pos', 0.5000),
+ ('NodeSocketMaterial', 'SeatMaterial', None)])
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Width"], 'Y': group_input.outputs["Thickness"], 'Z': 1.0000})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_1.outputs["Curve"], 'Translation': (0.0000, 0.0000, 0.5000), 'Scale': combine_xyz})
+
+ bent = nw.new_node(nodegroup_bent().name,
+ input_kwargs={'Geometry': transform_geometry_1, 'Amount': group_input.outputs["Seat Bent"]})
+
+ curve_circle_2 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["Mid Relative Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': group_input.outputs["Thickness"], 'Z': 1.0000})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_2.outputs["Curve"], 'Translation': (0.0000, 0.0000, 1.0000), 'Scale': combine_xyz_2})
+
+ bent_1 = nw.new_node(nodegroup_bent().name,
+ input_kwargs={'Geometry': transform_geometry_2, 'Amount': group_input.outputs["Mid Bent"]})
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_3.outputs["Curve"], 'Scale': (0.0000, 0.0050, 1.0000)})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["Front Relative Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_1, 'Y': 0.0050, 'Z': 1.0000})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Translation': (0.0000, 0.0000, 0.0600), 'Scale': combine_xyz_1})
+
+ bent_2 = nw.new_node(nodegroup_bent().name,
+ input_kwargs={'Geometry': transform_geometry, 'Amount': group_input.outputs["Front Bent"]})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [bent_1, bent, bent_2, transform_geometry_3]})
+
+ curve_circle_4 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["Back Relative Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Y': group_input.outputs["Thickness"], 'Z': 1.0000})
+
+ transform_geometry_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_4.outputs["Curve"], 'Translation': (0.0000, 0.0000, 1.5000), 'Scale': combine_xyz_3})
+
+ bent_3 = nw.new_node(nodegroup_bent().name,
+ input_kwargs={'Geometry': transform_geometry_4, 'Amount': group_input.outputs["Back Bent"]})
+
+ curve_circle_5 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["Top Relative Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': 0.0050, 'Z': 1.0000})
+
+ transform_geometry_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_5.outputs["Curve"], 'Translation': (0.0000, 0.0000, 2.0200), 'Scale': combine_xyz_4})
+
+ bent_4 = nw.new_node(nodegroup_bent().name,
+ input_kwargs={'Geometry': transform_geometry_5, 'Amount': group_input.outputs["Top Bent"]})
+
+ curve_circle_6 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["U Resolution"], 'Radius': 0.5000})
+
+ transform_geometry_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_6.outputs["Curve"], 'Translation': (0.0000, 0.0000, 2.1000), 'Scale': (0.0000, 0.0050, 1.0000)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_geometry_6, bent_4, bent_3]})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_2, join_geometry]})
+
+ lofting_001 = nw.new_node(nodegroup_lofting().name,
+ input_kwargs={'Profile Curves': join_geometry_1, 'U Resolution': group_input.outputs["U Resolution"], 'V Resolution': group_input.outputs["V Resolution"]})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_4, 'Z': 0.0300})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["Mid Pos"], 'Z': -0.0500})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_5, 'Z': group_input.outputs["Seat Height"]})
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Resolution': 128, 'Start': combine_xyz_6, 'Start Handle': combine_xyz_7, 'End Handle': (0.0000, 0.1000, 0.1000), 'End': combine_xyz_5})
+
+ warparoundcurvealt = nw.new_node(nodegroup_warp_around_curve().name,
+ input_kwargs={'Geometry': lofting_001.outputs["Geometry"], 'Curve': bezier_segment})
+
+ # material_func =np.random.choice([plastic.shader_rough_plastic, metal.get_shader(), wood_new.shader_wood, leather.shader_leather])
+
+ warparoundcurvealt = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': warparoundcurvealt, 'Material': group_input.outputs["SeatMaterial"]})
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': warparoundcurvealt}, attrs={'is_active_output': True})
+
diff --git a/infinigen/assets/seating/chairs/seats/round_seats.py b/infinigen/assets/seating/chairs/seats/round_seats.py
new file mode 100644
index 000000000..99959d77f
--- /dev/null
+++ b/infinigen/assets/seating/chairs/seats/round_seats.py
@@ -0,0 +1,41 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_top import nodegroup_capped_cylinder
+from infinigen.assets.materials.leather_and_fabrics.leather import shader_leather
+
+@node_utils.to_nodegroup('generate_round_seats', singleton=False, type='GeometryNodeTree')
+def generate_round_seats(nw: NodeWrangler, thickness=None, radius=None, cap_radius=None, bevel_factor=None, seat_material=None):
+ # Code generated using version 2.6.4 of the node_transpiler
+ if thickness is None:
+ thickness = uniform(0.05, 0.12)
+ if radius is None:
+ radius = uniform(0.35, 0.45)
+ if cap_radius is None:
+ cap_radius = uniform(2.0, 3.2)
+ if bevel_factor is None:
+ bevel_factor = uniform(0.01, 0.04)
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: thickness, 1: 1.0}, attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: bevel_factor, 1: thickness}, attrs={'operation': 'DIVIDE'})
+
+ cappedcylinder = nw.new_node(nodegroup_capped_cylinder().name,
+ input_kwargs={'Thickness': multiply, 'Radius': radius, 'Cap Flatness': cap_radius, 'Fillet Radius Vertical': divide, 'Cap Relative Scale': 0.0140, 'Cap Relative Z Offset': -0.0020, 'Resolution': 128})
+
+ seat = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': cappedcylinder, 'Material': seat_material})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': seat}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/seating/mattress.py b/infinigen/assets/seating/mattress.py
new file mode 100644
index 000000000..1d9a9612a
--- /dev/null
+++ b/infinigen/assets/seating/mattress.py
@@ -0,0 +1,126 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import fabrics
+from infinigen.assets.scatters import clothes
+from infinigen.assets.utils.decorate import (
+ subdivide_edge_ring, read_co,
+)
+from infinigen.assets.utils.object import new_bbox, new_cube
+from infinigen.core import surface
+from infinigen.core.nodes import NodeWrangler, Nodes
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import write_attr_data
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import random_general as rg
+from infinigen.assets.material_assignments import AssetList
+
+
+
+def make_coiled(obj, dot_distance, dot_depth, dot_size):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.poke()
+ bpy.ops.mesh.tris_convert_to_quads()
+ bpy.ops.mesh.poke()
+ bpy.ops.mesh.poke()
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bm = bmesh.from_edit_mesh(obj.data)
+ for v in bm.verts:
+ if len(v.link_edges) == 16:
+ v.select_set(True)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+ radius = dot_distance * uniform(.06, .08)
+ bpy.ops.mesh.bevel(offset=radius, affect='VERTICES')
+ bpy.ops.mesh.extrude_region_shrink_fatten(TRANSFORM_OT_shrink_fatten={'value': -dot_depth})
+ bpy.ops.mesh.extrude_region_shrink_fatten(TRANSFORM_OT_shrink_fatten={'value': dot_depth})
+ bpy.ops.mesh.select_more()
+ bpy.ops.mesh.select_more()
+ write_attr_data(obj, 'tip', np.zeros(len(obj.data.polygons)), domain='FACE')
+ surface.set_active(obj, 'tip')
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.attribute_set(value_float=1)
+
+ def geo_scale(nw: NodeWrangler):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ selection = nw.new_node(Nodes.NamedAttribute, ['tip'])
+ geometry = nw.new_node(
+ Nodes.ScaleElements,
+ [geometry, selection, nw.combine(*([dot_size / radius] * 3))]
+ )
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+ surface.add_geomod(obj, geo_scale, apply=True)
+ butil.modify_mesh(obj, 'TRIANGULATE', min_vertices=4)
+ butil.modify_mesh(obj, 'SMOOTH', factor=uniform(.5, 1.), iterations=5)
+
+
+class MattressFactory(AssetFactory):
+ types = 'weighted_choice', (1, 'coiled'), (1, 'wrapped')
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = log_uniform(.9, 2.)
+ self.size = uniform(2, 2.4)
+ self.thickness = uniform(.2, .35)
+ self.dot_distance = log_uniform(.16, .2)
+ self.dot_size = uniform(.005, .02)
+ self.dot_depth = uniform(.04, .08)
+ self.wrap_distance = .05
+ self.surface = fabrics
+ self.type= rg(self.types)
+ materials = AssetList['MattressFactory']()
+ self.surface = materials['surface'].assign_material()
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(
+ -self.width / 2, self.width / 2, -self.size / 2, self.size / 2, -self.thickness / 2,
+ self.thickness / 2
+ )
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_cube()
+ obj.scale = self.width / 2, self.size / 2, self.thickness / 2
+ butil.apply_transform(obj)
+ match self.type:
+ case 'coiled':
+ self.make_coiled(obj)
+ case 'wrapped':
+ self.make_wrapped(obj)
+ return obj
+
+ def make_coiled(self, obj):
+ for i, size in enumerate(obj.dimensions):
+ axis = np.zeros(3)
+ axis[i] = 1
+ subdivide_edge_ring(obj, int(np.ceil(size / self.dot_distance)), axis)
+ make_coiled(obj, self.dot_distance, self.dot_depth, self.dot_size)
+
+ def make_wrapped(self, obj):
+ for i, size in enumerate([self.width, self.size, self.thickness]):
+ axis = np.zeros(3)
+ axis[i] = 1
+ subdivide_edge_ring(obj, int(np.ceil(size / self.wrap_distance)), axis)
+ butil.modify_mesh(obj, 'BEVEL', width=self.wrap_distance / 3, segments=2)
+ vg = obj.vertex_groups.new(name='pin')
+ vg.add(np.nonzero((read_co(obj)[:, -1] < 1e-1 - self.thickness / 2))[0].tolist(), 1, 'REPLACE')
+ clothes.cloth_sim(
+ obj, gravity=0,
+ use_pressure=True,
+ uniform_pressure_force=uniform(.1, .2),
+ vertex_group_mass='pin'
+ )
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/seating/pillow.py b/infinigen/assets/seating/pillow.py
new file mode 100644
index 000000000..f5f871a24
--- /dev/null
+++ b/infinigen/assets/seating/pillow.py
@@ -0,0 +1,117 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import art, fabrics
+from infinigen.assets.scatters import clothes
+from infinigen.assets.utils.decorate import read_normal, read_selected, select_faces, subsurf, set_shade_smooth
+from infinigen.assets.utils.object import center, join_objects, new_base_circle, new_grid
+from infinigen.assets.utils.uv import unwrap_faces
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import random_general as rg
+from infinigen.assets.material_assignments import AssetList
+
+
+class PillowFactory(AssetFactory):
+ shapes = 'weighted_choice', (4, 'square'), (4, 'rectangle'), (1, 'circle'), (1, 'torus')
+
+ def __init__(self, factory_seed, coarse=False):
+ super(PillowFactory, self).__init__(factory_seed, coarse)
+ self.shape = rg(self.shapes)
+ self.width = uniform(.4, .7)
+ match self.shape:
+ case 'square':
+ self.size = self.width
+ case _:
+ self.size = self.width * log_uniform(.6, .8)
+ self.bevel_width = uniform(.02, .05)
+ self.thickness = log_uniform(.006, .008)
+ self.extrude_thickness = self.thickness * log_uniform(1, 8) if uniform() < .5 else 0
+ self.surface = np.random.choice([art.ArtFabric(self.factory_seed), fabrics])
+ self.has_seam = uniform() < .3 and not self.shape == 'torus'
+ self.seam_radius = uniform(.01, .02)
+
+ materials = AssetList['PillowFactory']()
+ self.surface = materials['surface'].assign_material()
+ if self.surface == art.ArtFabric:
+ self.surface = self.surface(self.factory_seed)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ match self.shape:
+ case 'circle':
+ obj = new_base_circle(vertices=128)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.fill_grid()
+ case 'torus':
+ obj = new_base_circle(vertices=128)
+ inner = new_base_circle(vertices=128, radius=uniform(.2, .4))
+ obj = join_objects([obj, inner])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bridge_edge_loops(number_cuts=12, interpolation='LINEAR')
+ obj = bpy.context.active_object
+ case _:
+ obj = new_grid(x_subdivisions=32, y_subdivisions=32)
+ obj.scale = self.width / 2, self.size / 2, 1
+ butil.apply_transform(obj, True)
+ unwrap_faces(obj)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=0)
+ normal = read_normal(obj)
+
+ group = obj.vertex_groups.new(name='pin')
+ if self.has_seam:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ select_faces(obj, lambda x, y, z: (x ** 2 + y ** 2 < self.seam_radius ** 2) & (z > 0))
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.select_mode(type='VERT')
+ selection = read_selected(obj)
+ group.add(np.nonzero(selection)[0].tolist(), 1, 'REPLACE')
+ select_faces(obj, np.abs(normal[:, -1]) < .1)
+
+ match self.shape:
+ case 'torus':
+ pressure = uniform(8, 12)
+ case _:
+ pressure = uniform(1, 2)
+ clothes.cloth_sim(
+ obj, tension_stiffness=uniform(0, 5),
+ gravity=0,
+ use_pressure=True,
+ uniform_pressure_force=pressure,
+ vertex_group_mass='pin' if self.has_seam else ""
+ )
+ if self.extrude_thickness > 0:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.extrude_region_shrink_fatten(
+ TRANSFORM_OT_shrink_fatten={'value': self.extrude_thickness}
+ )
+ obj.location = -center(obj)
+ butil.apply_transform(obj, True)
+ subsurf(obj, 2)
+ set_shade_smooth(obj)
+ return obj
+
+ def make_circle(self):
+ obj = new_base_circle(vertices=128)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.fill_grid()
+ select_faces(obj, lambda x, y, z: x ** 2 + y ** 2 < self.seam_radius ** 2)
+ bpy.ops.mesh.region_to_loop()
+ return obj
+
+ def make_gird(self):
+ obj = new_grid(x_subdivisions=64, y_subdivisions=64)
+ with butil.ViewportMode(obj, 'EDIT'):
+ select_faces(obj, lambda x, y, z: (np.abs(x) < self.seam_radius) & (np.abs(y) < self.seam_radius))
+ bpy.ops.mesh.region_to_loop()
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py
new file mode 100644
index 000000000..0f33fb326
--- /dev/null
+++ b/infinigen/assets/seating/sofa.py
@@ -0,0 +1,743 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Alexander Raistrick, Stamatis Alexandropolous, Yiming Zuo
+
+import bpy
+import bpy
+import mathutils
+import random
+
+import numpy as np
+from numpy.random import uniform, normal, randint, choice
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed
+from infinigen.core import tagging, tags as t
+
+from infinigen.core.util.random import log_uniform, clip_gaussian
+
+from infinigen.assets.material_assignments import AssetList
+
+@node_utils.to_nodegroup('nodegroup_array_fill_line', singleton=False, type='GeometryNodeTree')
+def nodegroup_array_fill_line(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVector', 'Line Start', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'Line End', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'Instance Dimensions', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketInt', 'Count', 10),
+ ('NodeSocketGeometry', 'Instance', None)])
+
+ multiply = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Instance Dimensions"], 1: (0.0000, -0.5000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: group_input.outputs["Line End"], 1: multiply.outputs["Vector"]})
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Line Start"], 1: multiply.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ mesh_line = nw.new_node(Nodes.MeshLine,
+ input_kwargs={'Count': group_input.outputs["Count"], 'Start Location': add.outputs["Vector"], 'Offset': subtract.outputs["Vector"]},
+ attrs={'mode': 'END_POINTS'})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': mesh_line, 'Instance': group_input.outputs["Instance"]})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_corner_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_corner_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVectorTranslation', 'Location', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVectorTranslation', 'CenteringLoc', (0.5000, 0.5000, 0.0000)),
+ ('NodeSocketVectorTranslation', 'Dimensions', (1.0000, 1.0000, 1.0000)),
+ ('NodeSocketFloat', 'SupportingEdgeFac', 0.0000),
+ ('NodeSocketInt', 'Vertices X', 4),
+ ('NodeSocketInt', 'Vertices Y', 4),
+ ('NodeSocketInt', 'Vertices Z', 4)])
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': group_input.outputs["Dimensions"], 'Vertices X': group_input.outputs["Vertices X"], 'Vertices Y': group_input.outputs["Vertices Y"], 'Vertices Z': group_input.outputs["Vertices Z"]})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Vector': group_input.outputs["CenteringLoc"], 9: (0.5000, 0.5000, 0.5000), 10: (-0.5000, -0.5000, -0.5000)},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ multiply_add = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: map_range.outputs["Vector"], 1: group_input.outputs["Dimensions"], 2: group_input.outputs["Location"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Translation': multiply_add.outputs["Vector"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': transform_geometry, 'Name': 'UVMap', 3: cube.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': store_named_attribute}, attrs={'is_active_output': True})
+
+ARM_TYPE_SQUARE = 0
+ARM_TYPE_ROUND = 1
+ARM_TYPE_ANGULAR = 2
+
+@node_utils.to_nodegroup('nodegroup_sofa_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_sofa_geometry(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketVector', 'Dimensions', (0.0000, 0.9000, 2.5000)),
+ ('NodeSocketVector', 'Arm Dimensions', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'Back Dimensions', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'Seat Dimensions', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'Foot Dimensions', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketFloat', 'Baseboard Height', 0.1300),
+ ('NodeSocketFloat', 'Backrest Width', 0.1100),
+ ('NodeSocketFloat', 'Seat Margin', 0.9700),
+ ('NodeSocketFloat', 'Backrest Angle', -0.2000),
+ ('NodeSocketFloatFactor', 'arm_width', 0.7000),
+ ('NodeSocketInt', 'Arm Type', 0),
+ ('NodeSocketFloatFactor', 'Arm_height', 0.7318),
+ ('NodeSocketFloatAngle', 'arms_angle', 0.8727),
+ ('NodeSocketBool', 'Footrest', False),
+ ('NodeSocketInt', 'Count', 4),
+ ('NodeSocketFloat', 'Scaling footrest', 1.5000),
+ ('NodeSocketInt', 'Reflection', 0),
+ ('NodeSocketBool', 'leg_type', False),
+ ('NodeSocketFloat', 'leg_dimensions', 0.5000),
+ ('NodeSocketFloat', 'leg_z', 1.0000),
+ ('NodeSocketInt', 'leg_faces', 20),
+ ('NodeSocketBool', 'Subdivide', True)])
+
+ multiply = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Dimensions"], 1: (0.0000, 0.5000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Arm Dimensions"]})
+
+ arm_cube = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'Location': multiply.outputs["Vector"], 'CenteringLoc': (0.0000, 1.0000, 0.0000), 'Dimensions': reroute, 'Vertices Z': 10},
+ label='ArmCube')
+
+ reroute_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': arm_cube})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': reroute})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': separate_xyz.outputs["Z"], 1: -0.1000, 2: separate_xyz_1.outputs["Z"], 3: -0.1000, 4: 0.2000})
+
+ float_curve = nw.new_node(Nodes.FloatCurve,
+ input_kwargs={'Factor': group_input.outputs["arm_width"], 'Value': map_range.outputs["Result"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], [(0.0092, 0.7688), (0.1011, 0.5937), (0.1494, 0.4062), (0.3954, 0.0781), (1.0000, 0.2187)])
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': multiply.outputs["Vector"]})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: separate_xyz_2.outputs["Y"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: float_curve, 1: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_14 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ map_range_1 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': separate_xyz_14.outputs["X"], 1: -1.0000, 2: 0.6000, 3: 2.1000, 4: -1.1000})
+
+ float_curve_1 = nw.new_node(Nodes.FloatCurve,
+ input_kwargs={'Factor': group_input.outputs["Arm_height"], 'Value': map_range_1.outputs["Result"]})
+ node_utils.assign_curve(float_curve_1.mapping.curves[0], [(0.1341, 0.2094), (0.7386, 1.0000), (0.9682, 0.0781), (1.0000, 0.0000)])
+
+ separate_xyz_15 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': (-2.9000, 3.3000, 0.0000)})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_14.outputs["Z"], 1: separate_xyz_15.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: float_curve_1, 1: subtract_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': multiply_2})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': combine_xyz, 'Axis': (1.0000, 0.0000, 0.0000), 'Angle': group_input.outputs["arms_angle"]})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': reroute_1, 'Offset': vector_rotate})
+
+ multiply_3 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Dimensions"], 1: (0.0000, 0.5000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ separate_xyz_3 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Arm Dimensions"]})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_3.outputs["Z"], 1: separate_xyz_3.outputs["Y"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz_3.outputs["X"], 'Y': separate_xyz_3.outputs["Y"], 'Z': subtract_2})
+
+ reroute_2 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': combine_xyz_1})
+
+ arm_cube_1 = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'Location': multiply_3.outputs["Vector"], 'CenteringLoc': (0.0000, 1.0000, 0.0000), 'Dimensions': reroute_2},
+ label='ArmCube')
+
+ separate_xyz_4 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': reroute_2})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_4.outputs["X"], 1: 1.0001}, attrs={'operation': 'MULTIPLY'})
+
+ reroute_3 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': multiply_4})
+
+ arm_cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Side Segments': 4, 'Radius': separate_xyz_4.outputs["Y"], 'Depth': reroute_3},
+ attrs={'fill_type': 'TRIANGLE_FAN'})
+
+ arm_cylinder = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': arm_cylinder.outputs["Mesh"], 'Name': 'UVMap', 3: arm_cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: reroute_3, 1: 2.0000}, attrs={'operation': 'DIVIDE'})
+
+ separate_xyz_5 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': multiply_3.outputs["Vector"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': divide, 'Y': separate_xyz_5.outputs["Y"], 'Z': separate_xyz_4.outputs["Z"]})
+
+ arm_cylinder = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': arm_cylinder, 'Translation': combine_xyz_2, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ roundtop = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [arm_cube_1, arm_cylinder]})
+
+ square_or_round = nw.new_node(
+ Nodes.Switch,
+ input_kwargs={
+ 'Switch': nw.compare('EQUAL', group_input.outputs['Arm Type'], ARM_TYPE_SQUARE),
+ 'False': roundtop,
+ 'True': arm_cube_1,
+ }
+ )
+
+ angular_or_squareround = nw.new_node(Nodes.Switch,
+ input_kwargs={
+ 'Switch': nw.compare('EQUAL', group_input.outputs['Arm Type'], ARM_TYPE_ANGULAR),
+ 'False': square_or_round,
+ 'True': set_position
+ }
+ )
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': angular_or_squareround, 'Scale': (1.0000, -1.0000, 1.0000)})
+
+ flip_faces = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': transform_geometry_1})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [flip_faces, angular_or_squareround]})
+
+ separate_xyz_6 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Back Dimensions"]})
+
+ separate_xyz_7 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Arm Dimensions"]})
+
+ separate_xyz_8 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Dimensions"]})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_7.outputs["Y"], 1: -2.0000, 2: separate_xyz_8.outputs["Y"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz_6.outputs["X"], 'Y': multiply_add, 'Z': separate_xyz_6.outputs["Z"]})
+
+ back_board = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': combine_xyz_3, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='BackBoard')
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_2, back_board]})
+
+ multiply_5 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: combine_xyz_3, 1: (1.0000, 0.0000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_add_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Arm Dimensions"], 1: (0.0000, -2.0000, 0.0000), 2: group_input.outputs["Dimensions"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_add_2 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Back Dimensions"], 1: (-1.0000, 0.0000, 0.0000), 2: multiply_add_1.outputs["Vector"]},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ separate_xyz_9 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': multiply_add_2.outputs["Vector"]})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz_9.outputs["X"], 'Y': separate_xyz_9.outputs["Y"], 'Z': group_input.outputs["Baseboard Height"]})
+
+ base_board = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'Location': multiply_5.outputs["Vector"], 'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': combine_xyz_4, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='BaseBoard')
+
+ reroute_13 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Count"]})
+
+ equal = nw.new_node(Nodes.Compare, input_kwargs={2: reroute_13, 3: 4}, attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ reroute_5 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': separate_xyz_9.outputs["Y"]})
+
+ separate_xyz_10 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Seat Dimensions"]})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_5, 1: separate_xyz_10.outputs["Y"]}, attrs={'operation': 'DIVIDE'})
+
+ ceil = nw.new_node(Nodes.Math, input_kwargs={0: divide_1}, attrs={'operation': 'CEIL'})
+
+ combine_xyz_14 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': ceil, 'Z': 1.0000})
+
+ divide_2 = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz_4, 1: combine_xyz_14}, attrs={'operation': 'DIVIDE'})
+
+ reroute_12 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': divide_2.outputs["Vector"]})
+
+ base_board_1 = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'Location': multiply_5.outputs["Vector"], 'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': reroute_12, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='BaseBoard')
+
+ equal_1 = nw.new_node(Nodes.Compare,
+ input_kwargs={0: 4.0000, 2: reroute_13, 3: 4},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ switch_8 = nw.new_node(Nodes.Switch,
+ input_kwargs={0: equal_1, 8: divide_2.outputs["Vector"], 9: combine_xyz_4},
+ attrs={'input_type': 'VECTOR'})
+
+ separate_xyz_16 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': switch_8.outputs[3]})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_16.outputs["Y"], 1: 0.7000}, attrs={'operation': 'MULTIPLY'})
+
+ grid_1 = nw.new_node(Nodes.MeshGrid, input_kwargs={'Size Y': multiply_6, 'Vertices X': 1, 'Vertices Y': 2})
+
+ combine_xyz_18 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': 0.1000, 'Y': separate_xyz_16.outputs["Y"], 'Z': separate_xyz_16.outputs["Z"]})
+
+ subtract_3 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: switch_8.outputs[3], 1: combine_xyz_18},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_7 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Back Dimensions"], 1: (1.0000, 0.0000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: subtract_3.outputs["Vector"], 1: multiply_7.outputs["Vector"]})
+
+ transform_geometry_10 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': grid_1.outputs["Mesh"], 'Translation': add.outputs["Vector"], 'Scale': (1.0000, 1.0000, 0.9000)})
+
+ cone = nw.new_node('GeometryNodeMeshCone',
+ input_kwargs={'Vertices': group_input.outputs["leg_faces"], 'Side Segments': 4, 'Radius Top': 0.0100, 'Radius Bottom': 0.0250, 'Depth': 0.0700})
+
+ reroute_9 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["leg_dimensions"]})
+
+ combine_xyz_17 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute_9, 'Y': reroute_9, 'Z': group_input.outputs["leg_z"]})
+
+ transform_geometry_9 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cone.outputs["Mesh"], 'Translation': (0.0000, 0.0000, 0.0100), 'Rotation': (0.0000, 3.1416, 0.0000), 'Scale': combine_xyz_17})
+
+ foot_cube = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'CenteringLoc': (0.5000, 0.5000, 0.9000), 'Dimensions': group_input.outputs["Foot Dimensions"]},
+ label='FootCube')
+
+ transform_geometry_12 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': foot_cube, 'Scale': (0.5000, 0.8000, 0.8000)})
+
+ switch_6 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["leg_type"], 14: transform_geometry_9, 15: transform_geometry_12})
+
+ transform_geometry_8 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': switch_6.outputs[6]})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': transform_geometry_10, 'Instance': transform_geometry_8, 'Scale': (1.0000, 1.0000, 1.2000)})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ join_geometry_10 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [base_board_1, realize_instances_1]})
+
+ subtract_4 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: combine_xyz_14, 1: (1.0000, 1.0000, 1.0000)},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_8 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: subtract_4.outputs["Vector"], 1: (0.0000, 0.5000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_9 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: divide_2.outputs["Vector"], 1: multiply_8.outputs["Vector"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_16 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': group_input.outputs["Reflection"], 'Z': 1.0000})
+
+ multiply_10 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: multiply_9.outputs["Vector"], 1: combine_xyz_16},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Scaling footrest"], 'Y': 1.0000, 'Z': 1.0000})
+
+ transform_geometry_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_10, 'Translation': multiply_10.outputs["Vector"], 'Scale': combine_xyz_12})
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Footrest"], 15: transform_geometry_5})
+
+ combine_xyz_19 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Scaling footrest"], 'Y': 1.3000, 'Z': 1.0000})
+
+ transform_geometry_11 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': realize_instances_1, 'Scale': combine_xyz_19})
+
+ base_board_2 = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'Location': multiply_5.outputs["Vector"], 'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': combine_xyz_4, 'Vertices X': 3, 'Vertices Y': 3, 'Vertices Z': 3},
+ label='BaseBoard')
+
+ combine_xyz_13 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Scaling footrest"], 'Y': 1.0000, 'Z': 1.0000})
+
+ transform_geometry_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': base_board_2, 'Scale': combine_xyz_13})
+
+ join_geometry_11 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_geometry_11, transform_geometry_6]})
+
+ switch_4 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Footrest"], 15: join_geometry_11})
+
+ switch_5 = nw.new_node(Nodes.Switch, input_kwargs={1: equal, 14: switch_2.outputs[6], 15: switch_4.outputs[6]})
+
+ join_geometry_4 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_3, base_board, switch_5.outputs[6]]})
+
+ grid = nw.new_node(Nodes.MeshGrid, input_kwargs={'Vertices X': 2, 'Vertices Y': 2})
+
+ multiply_11 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Dimensions"], 1: (0.5000, 0.0000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_12 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Dimensions"], 1: (1.0000, 1.0000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_13 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["Foot Dimensions"], 1: (2.5000, 2.5000, 0.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_5 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: multiply_12.outputs["Vector"], 1: multiply_13.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': grid.outputs["Mesh"], 'Translation': multiply_11.outputs["Vector"], 'Scale': subtract_5.outputs["Vector"]})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': transform_geometry_2, 'Instance': transform_geometry_8})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points})
+
+ join_geometry_5 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_4, realize_instances]})
+
+ reroute_10 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Count"]})
+
+ equal_2 = nw.new_node(Nodes.Compare,
+ input_kwargs={1: 4.0000, 2: reroute_10, 3: 4},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ reroute_4 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': combine_xyz_4})
+
+ multiply_14 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: reroute_4, 1: (0.0000, -0.5000, 1.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_15 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: reroute_4, 1: (0.0000, 0.5000, 1.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ equal_3 = nw.new_node(Nodes.Compare,
+ input_kwargs={1: 4.0000, 2: reroute_10, 3: 4},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ reroute_11 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Reflection"]})
+
+ switch_7 = nw.new_node(Nodes.Switch, input_kwargs={0: equal_3, 4: reroute_11, 5: 1}, attrs={'input_type': 'INT'})
+
+ combine_xyz_15 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': switch_7.outputs[1], 'Z': 1.1000})
+
+ multiply_16 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: multiply_15.outputs["Vector"], 1: combine_xyz_15},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide_3 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_5, 1: ceil}, attrs={'operation': 'DIVIDE'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz_10.outputs["X"], 'Y': divide_3, 'Z': separate_xyz_10.outputs["Z"]})
+
+ reroute_6 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': combine_xyz_5})
+
+ multiply_17 = nw.new_node(Nodes.VectorMath, input_kwargs={0: reroute_6, 1: combine_xyz_15}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_18 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: combine_xyz_5, 1: (1.0000, 1.0300, 1.0000)},
+ attrs={'operation': 'MULTIPLY'})
+
+ seat_cushion = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'CenteringLoc': (0.0000, 0.5000, 0.0000), 'Dimensions': multiply_18.outputs["Vector"], 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='SeatCushion')
+
+ upwards_part = nw.new_node(Nodes.Compare, input_kwargs={'A': nw.new_node(Nodes.Index), 'B': 2}, attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+ seat_cushion = tagging.tag_nodegroup(nw, seat_cushion, t.Subpart.SupportSurface, selection=upwards_part)
+
+ index = nw.new_node(Nodes.Index)
+
+ equal_4 = nw.new_node(Nodes.Compare, input_kwargs={2: index, 3: 1}, attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': seat_cushion, 'Selection': equal_4, 'Name': 'TAG_support', 6: True},
+ attrs={'data_type': 'BOOLEAN', 'domain': 'FACE'})
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 1.0000
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Selection': value, 'Name': 'TAG_cushion', 6: True},
+ attrs={'data_type': 'BOOLEAN', 'domain': 'FACE'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Seat Margin"], 'Y': group_input.outputs["Seat Margin"], 'Z': 1.0000})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute_2, 'Scale': combine_xyz_6})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Scaling footrest"], 'Y': 1.0000, 'Z': 1.1000})
+
+ transform_geometry_7 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_geometry_3, 'Scale': combine_xyz_11})
+
+ nodegroup_array_fill_line_002 = nw.new_node(nodegroup_array_fill_line().name,
+ input_kwargs={'Line Start': multiply_14.outputs["Vector"], 'Line End': multiply_16.outputs["Vector"], 'Instance Dimensions': multiply_17.outputs["Vector"], 'Count': reroute_10, 'Instance': transform_geometry_7})
+
+ separate_xyz_17 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': multiply_16.outputs["Vector"]})
+
+ combine_xyz_21 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': separate_xyz_17.outputs["Z"]})
+
+ reroute_14 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': ceil})
+
+ combine_xyz_20 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': reroute_14, 'Z': 1.0000})
+
+ transform_geometry_13 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_geometry_7, 'Scale': combine_xyz_20})
+
+ nodegroup_array_fill_line_002_1 = nw.new_node(nodegroup_array_fill_line().name,
+ input_kwargs={'Line End': combine_xyz_21, 'Count': 1, 'Instance': transform_geometry_13})
+
+ switch_9 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: equal_2, 14: nodegroup_array_fill_line_002, 15: nodegroup_array_fill_line_002_1})
+
+ switch_3 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Footrest"], 15: switch_9.outputs[6]})
+
+ nodegroup_array_fill_line_002_2 = nw.new_node(nodegroup_array_fill_line().name,
+ input_kwargs={'Line Start': multiply_14.outputs["Vector"], 'Line End': multiply_15.outputs["Vector"], 'Instance Dimensions': reroute_6, 'Count': reroute_14, 'Instance': transform_geometry_3})
+
+ join_geometry_9 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [switch_3.outputs[6], nodegroup_array_fill_line_002_2]})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': join_geometry_9, 'Level': 2})
+
+ separate_xyz_11 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Seat Dimensions"]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Backrest Width"], 'Z': separate_xyz_11.outputs["Z"]})
+
+ add_1 = nw.new_node(Nodes.VectorMath, input_kwargs={0: multiply_14.outputs["Vector"], 1: combine_xyz_7})
+
+ add_2 = nw.new_node(Nodes.VectorMath, input_kwargs={0: multiply_15.outputs["Vector"], 1: combine_xyz_7})
+
+ separate_xyz_12 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Dimensions"]})
+
+ subtract_6 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_12.outputs["Z"], 1: separate_xyz_11.outputs["Z"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ subtract_7 = nw.new_node(Nodes.Math,
+ input_kwargs={0: subtract_6, 1: group_input.outputs["Baseboard Height"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': subtract_7, 'Y': divide_3, 'Z': group_input.outputs["Backrest Width"]})
+
+ seat_cushion_1 = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'CenteringLoc': (0.1000, 0.5000, 1.0000), 'Dimensions': combine_xyz_8, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='SeatCushion')
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': seat_cushion_1, 'Offset Scale': 0.0300})
+
+ scale_elements = nw.new_node(Nodes.ScaleElements,
+ input_kwargs={'Geometry': extrude_mesh.outputs["Mesh"], 'Selection': extrude_mesh.outputs["Top"], 'Scale': 0.6000})
+
+ subdivision_surface_1 = nw.new_node(Nodes.SubdivisionSurface, input_kwargs={'Mesh': scale_elements})
+
+ random_value = nw.new_node(Nodes.RandomValue, attrs={'data_type': 'FLOAT_VECTOR'})
+
+ store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': subdivision_surface_1, 'Name': 'UVMap', 3: random_value.outputs["Value"]},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ multiply_19 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Backrest Width"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ separate_xyz_13 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': group_input.outputs["Back Dimensions"]})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz_13.outputs["X"], 1: 0.1000})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_19, 1: add_3})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Backrest Angle"], 1: -1.5708})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_5})
+
+ transform_geometry_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_3, 'Translation': combine_xyz_9, 'Rotation': combine_xyz_10, 'Scale': combine_xyz_6})
+
+ nodegroup_array_fill_line_003 = nw.new_node(nodegroup_array_fill_line().name,
+ input_kwargs={'Line Start': add_1.outputs["Vector"], 'Line End': add_2.outputs["Vector"], 'Instance Dimensions': reroute_6, 'Count': ceil, 'Instance': transform_geometry_4})
+
+ join_geometry_6 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [subdivide_mesh, nodegroup_array_fill_line_003]})
+
+ join_geometry_7 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_5, realize_instances, join_geometry_6]})
+
+ subdivide_mesh_1 = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': join_geometry_5, 'Level': 2})
+
+ join_geometry_8 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [subdivide_mesh_1, realize_instances, join_geometry_6]})
+
+ subdivision_surface_2 = nw.new_node(Nodes.SubdivisionSurface, input_kwargs={'Mesh': join_geometry_8, 'Level': 1})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: True, 14: join_geometry_7, 15: subdivision_surface_2})
+ switch = nw.new_node(Nodes.Switch, input_kwargs={
+ 1: group_input.outputs['Subdivide'],
+ 14: join_geometry_7,
+ 15: subdivision_surface_2
+ })
+
+ bounding_box = nw.new_node(nodegroup_corner_cube().name,
+ input_kwargs={'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': group_input.outputs["Dimensions"], 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2},
+ label='BoundingBox')
+
+ reroute_7 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': bounding_box})
+
+ reroute_8 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': reroute_7})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': switch_1.outputs[6], 'BoundingBox': reroute_8},
+ attrs={'is_active_output': True})
+
+
+
+def sofa_parameter_distribution(dimensions=None):
+
+ if dimensions is None:
+ dimensions = (
+ uniform(0.95, 1.1),
+ clip_gaussian(1.75, 0.75, 0.9, 3),
+ uniform(0.69, 0.97)
+ )
+
+ return {
+ 'Dimensions': dimensions,
+ 'Arm Dimensions': (
+ uniform(1, 1),
+ uniform(0.06, 0.15),
+ uniform(0.5, 0.75),
+ ),
+ 'Back Dimensions': (
+ uniform(0.15, 0.25),
+ 0.0000,
+ uniform(0.5, 0.75)
+ ),
+ 'Seat Dimensions': (
+ dimensions[0],
+ uniform(0.7, 1),
+ uniform(0.15, 0.3)
+ ),
+ 'Foot Dimensions': (
+ uniform(0.07, 0.25),
+ 0.06,
+ 0.06
+ ),
+ 'Baseboard Height': uniform(0.05, 0.09),
+ 'Backrest Width': uniform(0.1, 0.2),
+ 'Seat Margin': uniform(0.9700, 1),
+ 'Backrest Angle': uniform(-0.15, -0.5),
+
+ 'Arm Type': np.random.choice(
+ [ARM_TYPE_SQUARE, ARM_TYPE_ROUND, ARM_TYPE_ANGULAR],
+ p=[0.4, 0.2, 0.4]
+ ),
+ 'arm_width': uniform(0.6, 0.9),
+ 'Arm_height': uniform(0.7,1.0),
+ 'arms_angle': uniform(0.0, 1.08),
+ 'Footrest': True if uniform() > 0.5 and dimensions[1] > 2 else False,
+ 'Count': 1 if uniform()>0.2 else 4,
+ 'Scaling footrest': uniform(1.3, 1.6),
+ 'Reflection':1 if uniform()>0.5 else -1,
+ 'leg_type': True if uniform()>0.5 else False,
+ 'leg_dimensions': uniform(0.4,0.9),
+ 'leg_z':uniform(1.1, 2.5),
+ 'leg_faces':uniform(4,25)
+ }
+
+
+class SofaFactory(AssetFactory):
+ def __init__(self, factory_seed):
+ from infinigen.assets.clothes import blanket
+ super().__init__(factory_seed)
+ with FixedSeed(factory_seed):
+ self.params = sofa_parameter_distribution()
+ #from infinigen.assets.scatters.clothes import ClothesCover
+ #self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(1, 1.5),
+ # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply()
+ materials = AssetList['SofaFactory']()
+ self.sofa_fabric = materials['sofa_fabric'].assign_material()
+
+ def create_placeholder(self, **_):
+ obj = butil.spawn_vert()
+ butil.modify_mesh(
+ obj,
+ 'NODES',
+ node_group=nodegroup_sofa_geometry(),
+ ng_inputs={**self.params, },
+ apply=True
+ )
+ tagging.tag_system.relabel_obj(obj)
+ surface.add_material(obj, self.sofa_fabric)
+ return obj
+
+ def create_asset(self, i, placeholder, face_size, **_):
+
+ hipoly = butil.copy(placeholder, keep_materials=True)
+
+ butil.modify_mesh(hipoly, 'SUBSURF', levels=1, apply=True)
+
+ with butil.SelectObjects(hipoly):
+ bpy.ops.object.shade_smooth()
+
+ return hipoly
+
+class ArmChairFactory(SofaFactory):
+
+ def __init__(self, factory_seed):
+ super().__init__(factory_seed)
+ with FixedSeed(factory_seed):
+ dimensions = (
+ uniform(0.8, 1),
+ uniform(0.9, 1.1),
+ uniform(0.69, 0.97)
+ )
+ self.params = sofa_parameter_distribution(dimensions=dimensions)
\ No newline at end of file
diff --git a/infinigen/assets/shelves/__init__.py b/infinigen/assets/shelves/__init__.py
new file mode 100644
index 000000000..7fea19c8b
--- /dev/null
+++ b/infinigen/assets/shelves/__init__.py
@@ -0,0 +1,10 @@
+from .simple_desk import SimpleDeskFactory, SidetableDeskFactory
+from .simple_bookcase import SimpleBookcaseFactory
+from .cell_shelf import CellShelfFactory, TVStandFactory
+from .triangle_shelf import TriangleShelfFactory
+from .large_shelf import LargeShelfFactory
+from .doors import CabinetDoorBaseFactory
+from .single_cabinet import SingleCabinetFactory
+from .kitchen_cabinet import KitchenCabinetFactory
+from .kitchen_space import KitchenSpaceFactory, KitchenIslandFactory
+from .countertop import CountertopFactory
diff --git a/infinigen/assets/shelves/cabinet.py b/infinigen/assets/shelves/cabinet.py
new file mode 100644
index 000000000..517816530
--- /dev/null
+++ b/infinigen/assets/shelves/cabinet.py
@@ -0,0 +1,1003 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube, blender_rotate
+from infinigen.assets.shelves.large_shelf import LargeShelfBaseFactory, LargeShelfFactory, LargeShelfIkeaFactory
+from infinigen.assets.materials.shelf_shaders import get_shelf_material
+from infinigen.core.util.math import FixedSeed
+
+
+@node_utils.to_nodegroup('nodegroup_node_group', singleton=False, type='GeometryNodeTree')
+def nodegroup_node_group(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0120, 0.00060, 0.0400)})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': 0.0100, 'Depth': 0.00050})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': cylinder.outputs["Mesh"],
+ 'Translation': (0.0050, 0.0000, 0.0000),
+ 'Rotation': (1.5708, 0.0000, 0.0000)
+ })
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0200, 0.0006, 0.0120)})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cube_1, 'Translation': (0.0080, 0.0000, 0.0000)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [cube, transform, transform_1]})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'attach_height', 0.1000),
+ ('NodeSocketFloat', 'door_width', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["door_width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0181}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': subtract, 'Z': group_input.outputs["attach_height"]})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_knob_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_knob_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Radius', 0.0100),
+ ('NodeSocketFloat', 'thickness_1', 0.5000), ('NodeSocketFloat', 'thickness_2', 0.5000),
+ ('NodeSocketFloat', 'length', 0.5000), ('NodeSocketFloat', 'knob_mid_height', 0.0000),
+ ('NodeSocketFloat', 'edge_width', 0.5000), ('NodeSocketFloat', 'door_width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["thickness_2"], 1: group_input.outputs["thickness_1"]
+ })
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: group_input.outputs["length"]})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': group_input.outputs["Radius"], 'Depth': add_1
+ })
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["door_width"],
+ 1: group_input.outputs["edge_width"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.005})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': add_2,
+ 'Y': multiply_1,
+ 'Z': group_input.outputs["knob_mid_height"]
+ })
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': cylinder.outputs["Mesh"],
+ 'Translation': combine_xyz_6,
+ 'Rotation': (1.5708, 0.0000, 0.0000)
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_6},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_mid_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_mid_board(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000), ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_k = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_k = nw.new_node(Nodes.Math, input_kwargs={0: multiply_k, 1: 0.004})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.0001})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_3, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_1})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_4})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_4, 'Material': kwargs['material'][0]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_7, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5
+ })
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 1.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_2})
+
+ transform_7 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_8})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_7, 'Material': kwargs['material'][1]})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': realize_instances, 'mid_height': multiply},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_mid_board_001', singleton=False, type='GeometryNodeTree')
+def nodegroup_mid_board_001(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000), ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ multiply_k = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_k = nw.new_node(Nodes.Math, input_kwargs={0: multiply_k, 1: 0.004})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.0001})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_3, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_1})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_4})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_4, 'Material': kwargs['material'][0]})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': set_material})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': realize_instances, 'mid_height': multiply},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_double_rampled_edge', singleton=False, type='GeometryNodeTree')
+def nodegroup_double_rampled_edge(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness_2', 0.5000), ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'thickness_1', 0.5000), ('NodeSocketFloat', 'ramp_angle', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_10})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 3, 'Radius': 0.0100})
+
+ endpoint_selection = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ramp_angle"], 1: 0.0000})
+
+ tangent = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'TANGENT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_2"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: tangent, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: 2.0000, 1: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_1"], 1: 0.0000})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': add_4})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': curve_circle.outputs["Curve"],
+ 'Selection': endpoint_selection,
+ 'Position': combine_xyz_7
+ })
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: add_3})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': add_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': set_position,
+ 'Selection': endpoint_selection_1,
+ 'Position': combine_xyz_8
+ })
+
+ index = nw.new_node(Nodes.Index)
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_THAN'})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 0.9900},
+ attrs={'operation': 'GREATER_THAN'})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: less_than, 1: greater_than})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Y': add_4})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': set_position_1,
+ 'Selection': op_and,
+ 'Position': combine_xyz_9
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': curve_line,
+ 'Profile Curve': set_position_2,
+ 'Fill Caps': True
+ })
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': add_4, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_6})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': add_3, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_7})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_6})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_8})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_11})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_12})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': set_position_2, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': curve_line_1,
+ 'Profile Curve': transform_2,
+ 'Fill Caps': True
+ })
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_mesh, transform_4, curve_to_mesh_1]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance,
+ input_kwargs={'Geometry': join_geometry_1, 'Distance': 0.0001})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': merge_by_distance})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': realize_instances, 'Level': 4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': subdivide_mesh},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_ramped_edge', singleton=False, type='GeometryNodeTree')
+def nodegroup_ramped_edge(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness_2', 0.5000), ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'thickness_1', 0.5000), ('NodeSocketFloat', 'ramp_angle', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_10})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 3, 'Radius': 0.0100})
+
+ endpoint_selection = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ramp_angle"], 1: 0.0000})
+
+ tangent = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'TANGENT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_2"], 1: 0.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: tangent, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: subtract},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_1"], 1: 0.0000})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': add_4})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': curve_circle.outputs["Curve"],
+ 'Selection': endpoint_selection,
+ 'Position': combine_xyz_7
+ })
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: add_3})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': add_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': set_position,
+ 'Selection': endpoint_selection_1,
+ 'Position': combine_xyz_8
+ })
+
+ index = nw.new_node(Nodes.Index)
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_THAN'})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 0.9900},
+ attrs={'operation': 'GREATER_THAN'})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: less_than, 1: greater_than})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Y': add_4})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': set_position_1,
+ 'Selection': op_and,
+ 'Position': combine_xyz_9
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': curve_line,
+ 'Profile Curve': set_position_2,
+ 'Fill Caps': True
+ })
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': add_4, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': add_3, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_5})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_4, 'Y': add_6})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_6})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_11})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh, transform_4]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance,
+ input_kwargs={'Geometry': join_geometry_1, 'Distance': 0.0001})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': merge_by_distance})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': realize_instances, 'Level': 4})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_7})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': subdivide_mesh, 'Translation': combine_xyz_4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_panel_edge_frame', singleton=False, type='GeometryNodeTree')
+def nodegroup_panel_edge_frame(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'vertical_edge', None),
+ ('NodeSocketFloat', 'door_width', 0.5000), ('NodeSocketFloat', 'door_height', 0.0000),
+ ('NodeSocketGeometry', 'horizontal_edge', None)])
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["door_width"], 2: 0.0010},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ transform_7 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["horizontal_edge"],
+ 'Translation': (0.0000, -0.0001, 0.0000),
+ 'Scale': (0.9999, 1.0000, 1.0000)
+ })
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: 1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["door_height"], 1: 0.0001})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': add_1})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': transform_7,
+ 'Translation': combine_xyz_2,
+ 'Rotation': (0.0000, -1.5708, 0.0000)
+ })
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0001})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': transform_7,
+ 'Translation': combine_xyz_1,
+ 'Rotation': (0.0000, 1.5708, 0.0000)
+ })
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["vertical_edge"],
+ 'Translation': combine_xyz
+ })
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_3, transform_2, transform_1, transform]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Value': multiply, 'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+def geometry_door_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ door_height = nw.new_node(Nodes.Value, label='door_height')
+ door_height.outputs[0].default_value = kwargs['door_height']
+
+ door_edge_thickness_2 = nw.new_node(Nodes.Value, label='door_edge_thickness_2')
+ door_edge_thickness_2.outputs[0].default_value = kwargs['edge_thickness_2']
+
+ door_edge_width = nw.new_node(Nodes.Value, label='door_edge_width')
+ door_edge_width.outputs[0].default_value = kwargs['edge_width']
+
+ door_edge_thickness_1 = nw.new_node(Nodes.Value, label='door_edge_thickness_1')
+ door_edge_thickness_1.outputs[0].default_value = kwargs['edge_thickness_1']
+
+ door_edge_ramp_angle = nw.new_node(Nodes.Value, label='door_edge_ramp_angle')
+ door_edge_ramp_angle.outputs[0].default_value = kwargs['edge_ramp_angle']
+
+ ramped_edge = nw.new_node(nodegroup_ramped_edge().name, input_kwargs={
+ 'height': door_height,
+ 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width,
+ 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle
+ })
+
+ door_width = nw.new_node(Nodes.Value, label='door_width')
+ door_width.outputs[0].default_value = kwargs['door_width']
+
+ ramped_edge_1 = nw.new_node(nodegroup_ramped_edge().name, input_kwargs={
+ 'height': door_width,
+ 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width,
+ 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle
+ })
+
+ panel_edge_frame = nw.new_node(nodegroup_panel_edge_frame().name, input_kwargs={
+ 'vertical_edge': ramped_edge,
+ 'door_width': door_width,
+ 'door_height': door_height,
+ 'horizontal_edge': ramped_edge_1
+ })
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: panel_edge_frame.outputs["Value"], 1: 0.0001})
+
+ mid_board_thickness = nw.new_node(Nodes.Value, label='mid_board_thickness')
+ mid_board_thickness.outputs[0].default_value = kwargs['board_thickness']
+
+ if kwargs['has_mid_ramp']:
+ mid_board = nw.new_node(nodegroup_mid_board(material=kwargs['board_material']).name, input_kwargs={
+ 'height': door_height,
+ 'thickness': mid_board_thickness,
+ 'width': door_width
+ })
+ else:
+ mid_board = nw.new_node(nodegroup_mid_board_001(material=kwargs['board_material']).name, input_kwargs={
+ 'height': door_height,
+ 'thickness': mid_board_thickness,
+ 'width': door_width
+ })
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': add, 'Y': -0.0001, 'Z': mid_board.outputs["mid_height"]})
+
+ frame = [panel_edge_frame.outputs["Geometry"]]
+ if kwargs['has_mid_ramp']:
+ double_rampled_edge = nw.new_node(nodegroup_double_rampled_edge().name, input_kwargs={
+ 'height': door_width,
+ 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width,
+ 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle
+ })
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': double_rampled_edge,
+ 'Translation': combine_xyz_5,
+ 'Rotation': (0.0000, 1.5708, 0.0000)
+ })
+ frame.append(transform_5)
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': frame})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_1, 'Material': kwargs['frame_material']
+ })
+
+ knob_raduis = nw.new_node(Nodes.Value, label='knob_raduis')
+ knob_raduis.outputs[0].default_value = kwargs['knob_R']
+
+ know_length = nw.new_node(Nodes.Value, label='know_length')
+ know_length.outputs[0].default_value = kwargs['knob_length']
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: door_height}, attrs={'operation': 'MULTIPLY'})
+
+ knob_handle = nw.new_node(nodegroup_knob_handle().name, input_kwargs={
+ 'Radius': knob_raduis,
+ 'thickness_1': door_edge_thickness_1,
+ 'thickness_2': door_edge_thickness_2,
+ 'length': know_length,
+ 'knob_mid_height': multiply,
+ 'edge_width': door_edge_width,
+ 'door_width': door_width
+ })
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': knob_handle, 'Material': kwargs['frame_material']})
+
+ attach_gadgets = []
+
+ for h in kwargs['attach_height']:
+ attach_height = nw.new_node(Nodes.Value, label='attach_height')
+ attach_height.outputs[0].default_value = h
+
+ attach = nw.new_node(nodegroup_node_group().name,
+ input_kwargs={'attach_height': attach_height, 'door_width': door_width})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': attach, 'Material': get_shelf_material('metal')})
+ attach_gadgets.append(set_material_1)
+
+ geos = [set_material_2, set_material_3, mid_board.outputs["Geometry"]] + attach_gadgets
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': geos})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: door_width, 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': transform})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': triangulate,
+ 'Scale': (-1.0 if kwargs['door_left_hinge'] else 1.0, 1.0000, 1.0000)
+ })
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_1, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+def geometry_cabinet_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ right_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['door'][0]})
+ left_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['door'][1]})
+ shelf_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['shelf']})
+
+ doors = []
+ transform_r = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': right_door_info.outputs['Geometry'],
+ 'Translation': kwargs['door_hinge_pos'][0],
+ 'Rotation': (0, 0, kwargs['door_open_angle'])
+ })
+ doors.append(transform_r)
+ if len(kwargs['door_hinge_pos']) > 1:
+ transform_l = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': left_door_info.outputs['Geometry'],
+ 'Translation': kwargs['door_hinge_pos'][1],
+ 'Rotation': (0, 0, kwargs['door_open_angle'])
+ })
+ doors.append(transform_l)
+
+ attaches = []
+ for pos in kwargs['attach_pos']:
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0006, 0.0200, 0.04500)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': -0.0100})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0005, 0.0340, 0.0200)})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, cube_1]})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': join_geometry,
+ 'Translation': (0.0000, -0.0170, 0.0000)
+ })
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_1, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_2, 'Translation': pos})
+
+ attaches.append(transform_3)
+
+ join_geometry_a = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': attaches})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_a, 'Material': get_shelf_material('metal')})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [shelf_info.outputs['Geometry']] + doors + [set_material]
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+class CabinetDoorBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(CabinetDoorBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {}
+
+ def get_asset_params(self, i=0):
+ params = self.params.copy()
+ if params.get('door_height', None) is None:
+ params['door_height'] = uniform(0.7, 2.2)
+ if params.get('door_width', None) is None:
+ params['door_width'] = uniform(0.3, 0.4)
+ if params.get('edge_thickness_1', None) is None:
+ params['edge_thickness_1'] = uniform(0.01, 0.02)
+ if params.get('edge_width', None) is None:
+ params['edge_width'] = uniform(0.03, 0.05)
+ if params.get('edge_thickness_2', None) is None:
+ params['edge_thickness_2'] = uniform(0.005, 0.01)
+ if params.get('edge_ramp_angle', None) is None:
+ params['edge_ramp_angle'] = uniform(0.6, 0.8)
+ params['board_thickness'] = params['edge_thickness_1'] - 0.005
+ if params.get('knob_R', None) is None:
+ params['knob_R'] = uniform(0.003, 0.006)
+ if params.get('knob_length', None) is None:
+ params['knob_length'] = uniform(0.018, 0.035)
+ if params.get('attach_height', None) is None:
+ gap = uniform(0.05, 0.15)
+ params['attach_height'] = [gap, params['door_height'] - gap]
+ if params.get('has_mid_ramp', None) is None:
+ params['has_mid_ramp'] = np.random.choice([True, False], p=[0.6, 0.4])
+ if params.get('door_left_hinge', None) is None:
+ params['door_left_hinge'] = False
+
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.5, 0.2, 0.3])
+ if params.get('board_material', None) is None:
+ if params['has_mid_ramp']:
+ lower_mat = np.random.choice([params['frame_material'], 'glass'], p=[0.7, 0.3])
+ upper_mat = np.random.choice([lower_mat, 'glass'], p=[0.6, 0.4])
+ params['board_material'] = [lower_mat, upper_mat]
+ else:
+ params['board_material'] = [params['frame_material']]
+
+ params = self.get_material_func(params)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ params['frame_material'] = get_shelf_material(params['frame_material'])
+ materials = []
+ if not isinstance(params['board_material'], list):
+ params['board_material'] = [params['board_material']]
+ for mat in params['board_material']:
+ materials.append(get_shelf_material(mat))
+ params['board_material'] = materials
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_door_nodes, apply=True, attributes=[], input_kwargs=obj_params)
+
+ if params.get('ret_params', False):
+ return obj, obj_params
+
+ return obj
+
+
+class CabinetDoorIkeaFactory(CabinetDoorBaseFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(CabinetDoorIkeaFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {
+ 'edge_thickness_1': 0.012,
+ 'edge_thickness_2': 0.008,
+ 'board_thickness': 0.006,
+ 'edge_width': 0.02,
+ 'edge_ramp_angle': 0.5,
+ 'knob_R': 0.004,
+ 'knob_length': 0.03,
+ 'has_mid_ramp': False,
+ 'attach_height': 0.08
+ }
+
+ def get_asset_params(self, i=0):
+ params = self.params.copy()
+ if params.get('door_height', None) is None:
+ params['door_height'] = uniform(0.7, 2.2)
+ if params.get('door_width', None) is None:
+ params['door_width'] = uniform(0.3, 0.4)
+ if params.get('door_left_hinge', None) is None:
+ params['door_left_hinge'] = False
+
+ params['attach_height'] = [params['door_height'] - params['attach_height'], params['attach_height']]
+ params = self.get_material_func(params)
+ return params
+
+
+class CabinetBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(CabinetBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.shelf_params = {}
+ self.door_params = {}
+ self.mat_params = {}
+ self.shelf_fac = LargeShelfBaseFactory(factory_seed)
+ self.door_fac = CabinetDoorBaseFactory(factory_seed)
+
+ def sample_params(self):
+ # Update fac params
+ pass
+
+ def get_material_params(self):
+ with FixedSeed(self.factory_seed):
+ params = self.mat_params.copy()
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.5, 0.2, 0.3])
+ return params
+
+ def get_shelf_params(self, i=0):
+ params = self.shelf_params.copy()
+ if params.get('shelf_cell_width', None) is None:
+ params['shelf_cell_width'] = [
+ np.random.choice([0.76, 0.36], p=[0.5, 0.5]) * np.clip(normal(1., 0.1), 0.75, 1.25)]
+ if params.get('shelf_cell_height', None) is None:
+ num_v_cells = randint(3, 7)
+ shelf_cell_height = []
+ for i in range(num_v_cells):
+ shelf_cell_height.append(0.3 * np.clip(normal(1., 0.06), 0.75, 1.25))
+ params['shelf_cell_height'] = shelf_cell_height
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = self.mat_params['frame_material']
+
+ return params
+
+ def get_door_params(self, i=0):
+ params = self.door_params.copy()
+
+ # get door params
+ shelf_width = self.shelf_params['shelf_width'] + self.shelf_params['side_board_thickness'] * 2
+ if params.get('door_width', None) is None:
+ if shelf_width < 0.55:
+ params['door_width'] = shelf_width
+ params['num_door'] = 1
+ else:
+ params['door_width'] = shelf_width / 2. - 0.0005
+ params['num_door'] = 2
+ if params.get('door_height', None) is None:
+ params['door_height'] = (self.shelf_params['division_board_z_translation'][-1] -
+ self.shelf_params['division_board_z_translation'][0] + self.shelf_params[
+ 'division_board_thickness'])
+ if len(self.shelf_params['division_board_z_translation']) > 5 and np.random.choice([True, False],
+ p=[0.5, 0.5]):
+ params['door_height'] = (self.shelf_params['division_board_z_translation'][3] -
+ self.shelf_params['division_board_z_translation'][0] +
+ self.shelf_params['division_board_thickness'])
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = self.mat_params['frame_material']
+
+ return params
+
+ def get_cabinet_params(self, i=0):
+ params = dict()
+
+ shelf_width = self.shelf_params['shelf_width'] + self.shelf_params['side_board_thickness'] * 2
+ if self.door_params['num_door'] == 1:
+ params['door_hinge_pos'] = [(self.shelf_params['shelf_depth'] / 2. + 0.0025, -shelf_width / 2.,
+ self.shelf_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+ params['attach_pos'] = [(
+ self.shelf_params['shelf_depth'] / 2., -self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']]
+ elif self.door_params['num_door'] == 2:
+ params['door_hinge_pos'] = [(self.shelf_params['shelf_depth'] / 2. + 0.008, -shelf_width / 2.,
+ self.shelf_params['bottom_board_height']), (
+ self.shelf_params['shelf_depth'] / 2. + 0.008, shelf_width / 2.,
+ self.shelf_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+ params['attach_pos'] = [(
+ self.shelf_params['shelf_depth'] / 2., -self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']] + [(
+ self.shelf_params['shelf_depth'] / 2., self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']]
+ else:
+ raise NotImplementedError
+
+ return params
+
+ def get_cabinet_components(self, i):
+ # update material params
+ self.sample_params()
+ self.mat_params = self.get_material_params()
+
+ # create shelf
+ shelf_params = self.get_shelf_params(i=i)
+ self.shelf_fac.params = shelf_params
+ shelf, shelf_params = self.shelf_fac.create_asset(i=i, ret_params=True)
+ shelf.name = 'cabinet_frame'
+ self.shelf_params = shelf_params
+
+ # create doors
+ door_params = self.get_door_params(i=i)
+ self.door_fac.params = door_params
+ self.door_fac.params['door_left_hinge'] = False
+ right_door, door_obj_params = self.door_fac.create_asset(i=i, ret_params=True)
+ right_door.name = 'cabinet_right_door'
+ self.door_fac.params = door_obj_params
+ self.door_fac.params['door_left_hinge'] = True
+ left_door, _ = self.door_fac.create_asset(i=i, ret_params=True)
+ left_door.name = 'cabinet_left_door'
+ self.door_params = door_obj_params
+
+ return shelf, right_door, left_door
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ shelf, right_door, left_door = self.get_cabinet_components(i=i)
+
+ # create cabinet
+ cabinet_params = self.get_cabinet_params(i=i)
+ surface.add_geomod(obj, geometry_cabinet_nodes, attributes=[], input_kwargs={
+ 'door': [right_door, left_door],
+ 'shelf': shelf,
+ 'door_hinge_pos': cabinet_params['door_hinge_pos'],
+ 'door_open_angle': cabinet_params['door_open_angle'],
+ 'attach_pos': cabinet_params['attach_pos']
+ })
+ butil.delete([shelf, left_door, right_door])
+ return obj
+
+
+class CabinetFactory(CabinetBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (uniform(0.25, 0.35), uniform(0.3, 0.7), uniform(0.9, 1.8))
+
+ params['bottom_board_height'] = 0.083
+ params['shelf_depth'] = params['Dimensions'][0] - 0.01
+ num_h = int((params['Dimensions'][2] - 0.083) / 0.3)
+ params['shelf_cell_height'] = [(params['Dimensions'][2] - 0.083) / num_h for _ in range(num_h)]
+ params['shelf_cell_width'] = [params['Dimensions'][1]]
+ self.shelf_params = self.shelf_fac.sample_params()
+
diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py
new file mode 100644
index 000000000..781e36496
--- /dev/null
+++ b/infinigen/assets/shelves/cell_shelf.py
@@ -0,0 +1,900 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+import numpy as np
+import bpy
+
+from infinigen.assets.materials import metal
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.core.util import blender as butil, math as mu
+from infinigen.core import tagging, tags as t
+
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_wood, shader_shelves_wood_sampler,
+ shader_shelves_white_metallic, shader_shelves_white_metallic_sampler,
+ shader_shelves_black_metallic, shader_shelves_black_metallic_sampler)
+
+from infinigen.assets.utils.object import new_bbox
+from infinigen.core.util.math import FixedSeed
+
+@node_utils.to_nodegroup('nodegroup_screw_head', singleton=False, type='GeometryNodeTree')
+def nodegroup_screw_head(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Radius': 0.0050, 'Depth': 0.0010})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Z', 0.5000),
+ ('NodeSocketFloat', 'leg', 0.5000),
+ ('NodeSocketFloat', 'X', 0.5000),
+ ('NodeSocketFloat', 'external', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["external"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["X"], 1: add},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Z"], 1: group_input.outputs["leg"]})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_2})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': multiply_1, 'Z': add_2})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["depth"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': subtract_1, 'Z': add_2})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_1})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': subtract_1, 'Z': add_2})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_2})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': multiply_1, 'Z': add_2})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_3})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_2, transform_3, transform_4, transform_5]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_3},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_base_frame', singleton=False, type='GeometryNodeTree')
+def nodegroup_base_frame(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'leg_height', 0.5000),
+ ('NodeSocketFloat', 'leg_size', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'bottom_x', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_size"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add, 'Z': add_1})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["bottom_x"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': multiply_1, 'Z': multiply_2})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': multiply_1, 'Z': multiply_2})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: 0.0000})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': subtract_1, 'Z': multiply_2})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_3})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': subtract_1, 'Z': multiply_2})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_4})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: multiply_4}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_2, 'Y': add, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_5, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ subtract_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': subtract_3})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_6})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: 0.0000})
+
+ subtract_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_5, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': subtract_4, 'Z': subtract_3})
+
+ transform_7 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_7})
+
+ subtract_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: multiply_4}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': subtract_5, 'Z': add})
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_8, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ subtract_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_6}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_5}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_6, 1: add})
+
+ subtract_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Y': add_6, 'Z': subtract_7})
+
+ transform_8 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_2, 'Translation': combine_xyz_9})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_5, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_7, 'Y': add_6, 'Z': subtract_7})
+
+ transform_9 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_2, 'Translation': combine_xyz_10})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={
+ 'Geometry': [transform_2, transform_3, transform_4, transform_5, transform_6,
+ transform_7, transform_8, transform_9]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_3},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_back_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_back_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'X', 0.0000),
+ ('NodeSocketFloat', 'Z', 0.5000),
+ ('NodeSocketFloat', 'leg', 0.5000),
+ ('NodeSocketFloat', 'external', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Z"], 1: 0.0000})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["X"], 'Y': 0.01, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: group_input.outputs["leg"]})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: group_input.outputs["external"]})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add_2})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_5})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_6},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_attach_gadget', singleton=False, type='GeometryNodeTree')
+def nodegroup_attach_gadget(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'z', 0.5000),
+ ('NodeSocketFloat', 'base_leg', 0.5000),
+ ('NodeSocketFloat', 'x', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'size', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["size"], 1: 0.0000})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': 0.0010, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_4})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["thickness"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["base_leg"], 1: group_input.outputs["z"]})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1 , 1: group_input.outputs["thickness"]})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: -0.02})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Z': subtract_2})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_5})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Z': subtract_2})
+
+ transform_7 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_6})
+
+ join_geometry_5 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_6, transform_7]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_5},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_h_division_placement', singleton=False, type='GeometryNodeTree')
+def nodegroup_h_division_placement(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'cell_size', 0.5000),
+ ('NodeSocketFloat', 'leg_height', 0.5000),
+ ('NodeSocketFloat', 'division_board_thickness', 0.5000),
+ ('NodeSocketFloat', 'external_board_thickness', 0.5000),
+ ('NodeSocketFloat', 'index', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["index"], 1: 0.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: group_input.outputs["cell_size"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -1.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["external_board_thickness"], 1: 0.0000})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: multiply_2})
+
+ add_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["division_board_thickness"],
+ 1: group_input.outputs["leg_height"]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_3})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: add_5})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply, 'Z': add_6})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Vector': combine_xyz},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_h_division_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_h_division_board(nw: NodeWrangler, tag_support=False):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'cell_size', 0.5000),
+ ('NodeSocketFloat', 'horizontal_cell_num', 0.5000),
+ ('NodeSocketFloat', 'division_board_thickness', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.0000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["horizontal_cell_num"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: group_input.outputs["cell_size"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -1.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: add_1, 1: group_input.outputs["division_board_thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: multiply_1})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["division_board_thickness"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': add_2, 'Y': group_input.outputs["depth"], 'Z': add_3})
+ if tag_support:
+ cube = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz})
+ else:
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': cube}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_v_division_board_placement', singleton=False, type='GeometryNodeTree')
+def nodegroup_v_division_board_placement(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'base_leg', 0.5000),
+ ('NodeSocketFloat', 'external_thickness', 0.5000),
+ ('NodeSocketFloat', 'side_z', 0.5000),
+ ('NodeSocketFloat', 'index', 0.5000),
+ ('NodeSocketFloat', 'h_cell_num', 0.5000),
+ ('NodeSocketFloat', 'division_thickness', 0.5000),
+ ('NodeSocketFloat', 'cell_size', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["h_cell_num"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -1.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={1: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["index"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: add_2}, attrs={'operation': 'SUBTRACT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: subtract})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: add_3, 1: group_input.outputs["division_thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: add_2}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["cell_size"], 1: subtract_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: multiply_3})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["base_leg"], 1: group_input.outputs["external_thickness"]})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["side_z"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_5, 1: multiply_5})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': multiply_4, 'Z': add_6})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Vector': combine_xyz_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_v_division_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_v_division_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'division_board_thickness', 0.0000),
+ ('NodeSocketFloat', 'depth', 0.0000),
+ ('NodeSocketFloat', 'cell_size', 0.5000),
+ ('NodeSocketFloat', 'vertical_cell_num', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["vertical_cell_num"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["cell_size"], 1: add},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 1.0000}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: subtract, 1: group_input.outputs["division_board_thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: multiply_1})
+
+ add_200 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: -0.001})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["division_board_thickness"],
+ 'Y': add_200, 'Z': add_1})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': cube, 'Value': add_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_top_bottom_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_top_bottom_board(nw: NodeWrangler, tag_support=False):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'base_leg_height', 0.5000),
+ ('NodeSocketFloat', 'horizontal_cell_num', 0.5000),
+ ('NodeSocketFloat', 'vertical_cell_num', 0.5000),
+ ('NodeSocketFloat', 'cell_size', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'division_board_thickness', 0.5000),
+ ('NodeSocketFloat', 'external_board_thickness', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["external_board_thickness"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["division_board_thickness"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["horizontal_cell_num"], 1: 0.0000})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: -1.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: multiply_1})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["cell_size"], 1: 0.0000})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_5, 1: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_2})
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_6, 1: 0.0020})
+
+ add_8 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: add_8, 1: 0.0000})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_7, 'Y': add_9, 'Z': add})
+
+ if tag_support:
+ cube_1 = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz_3})
+ else:
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_3, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_8}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ add_10 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: group_input.outputs["base_leg_height"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3, 'Z': add_10})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz})
+
+ add_11 = nw.new_node(Nodes.Math, input_kwargs={0: add_10, 1: add})
+
+ add_12 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["vertical_cell_num"], 1: 0.0000})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_12, 1: add_5}, attrs={'operation': 'MULTIPLY'})
+
+ add_13 = nw.new_node(Nodes.Math, input_kwargs={0: add_11, 1: multiply_5})
+
+ add_14 = nw.new_node(Nodes.Math, input_kwargs={0: add_12, 1: -1.0000})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: add_14}, attrs={'operation': 'MULTIPLY'})
+
+ add_15 = nw.new_node(Nodes.Math, input_kwargs={0: add_13, 1: multiply_6})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3, 'Z': add_15})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_1})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': join_geometry_1, 'x': add_7},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_side_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_side_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'base_leg_height', 0.5000),
+ ('NodeSocketFloat', 'horizontal_cell_num', 0.5000),
+ ('NodeSocketFloat', 'vertical_cell_num', 0.5000),
+ ('NodeSocketFloat', 'cell_size', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'division_thickness', 0.5000),
+ ('NodeSocketFloat', 'external_thickness', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["external_thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["vertical_cell_num"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: 1.0000}, attrs={'operation': 'SUBTRACT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["division_thickness"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["cell_size"], 1: 0.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: multiply_1})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_5})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: add_4, 1: group_input.outputs["horizontal_cell_num"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["horizontal_cell_num"], 1: 1.0000},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: subtract_1}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: multiply_3})
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: add_6})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={1: add_7}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_5}, attrs={'operation': 'MULTIPLY'})
+
+ add_8 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_6, 1: group_input.outputs["base_leg_height"]})
+
+ add_9 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["external_thickness"], 1: add_8})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_4, 'Y': multiply_5, 'Z': add_9})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_7, 'Y': multiply_5, 'Z': add_9})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ base_leg_height = nw.new_node(Nodes.Value, label='base_leg_height')
+ base_leg_height.outputs[0].default_value = kwargs['base_leg_height']
+
+ horizontal_cell_num = nw.new_node(Nodes.Integer, label='horizontal_cell_num')
+ horizontal_cell_num.integer = kwargs['horizontal_cell_num']
+
+ vertical_cell_num = nw.new_node(Nodes.Integer, label='vertical_cell_num')
+ vertical_cell_num.integer = kwargs['vertical_cell_num']
+
+ cell_size = nw.new_node(Nodes.Value, label='cell_size')
+ cell_size.outputs[0].default_value = kwargs['cell_size']
+
+ depth = nw.new_node(Nodes.Value, label='depth')
+ depth.outputs[0].default_value = kwargs['depth']
+
+ division_board_thickness = nw.new_node(Nodes.Value, label='division_board_thickness')
+ division_board_thickness.outputs[0].default_value = kwargs['division_board_thickness']
+
+ external_board_thickness = nw.new_node(Nodes.Value, label='external_board_thickness')
+ external_board_thickness.outputs[0].default_value = kwargs['external_board_thickness']
+
+ sideboard = nw.new_node(nodegroup_side_board().name,
+ input_kwargs={'base_leg_height': base_leg_height,
+ 'horizontal_cell_num': horizontal_cell_num,
+ 'vertical_cell_num': vertical_cell_num, 'cell_size': cell_size,
+ 'depth': depth, 'division_thickness': division_board_thickness,
+ 'external_thickness': external_board_thickness})
+
+ topbottomboard = nw.new_node(nodegroup_top_bottom_board(tag_support=kwargs.get('tag_support', False)).name,
+ input_kwargs={'base_leg_height': base_leg_height,
+ 'horizontal_cell_num': horizontal_cell_num,
+ 'vertical_cell_num': vertical_cell_num, 'cell_size': cell_size,
+ 'depth': depth, 'division_board_thickness': division_board_thickness,
+ 'external_board_thickness': external_board_thickness})
+
+ vdivisionboard = nw.new_node(nodegroup_v_division_board().name,
+ input_kwargs={'division_board_thickness': division_board_thickness, 'depth': depth,
+ 'cell_size': cell_size, 'vertical_cell_num': vertical_cell_num})
+
+ all_components = [sideboard, topbottomboard.outputs["Geometry"]]
+
+ v_division_boards = []
+ for i in range(1, kwargs['horizontal_cell_num']):
+ v_division_index = nw.new_node(Nodes.Integer, label='VDivisionIndex')
+ v_division_index.integer = i
+
+ vdivisionboardplacement = nw.new_node(nodegroup_v_division_board_placement().name,
+ input_kwargs={'depth': depth, 'base_leg': base_leg_height,
+ 'external_thickness': external_board_thickness,
+ 'side_z': vdivisionboard.outputs["Value"],
+ 'index': v_division_index, 'h_cell_num': horizontal_cell_num,
+ 'division_thickness': division_board_thickness,
+ 'cell_size': cell_size})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': vdivisionboard.outputs["Mesh"],
+ 'Translation': vdivisionboardplacement})
+ v_division_boards.append(transform_1)
+
+ if len(v_division_boards) > 0:
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': v_division_boards})
+ all_components.append(join_geometry_1)
+
+ hdivisionboard = nw.new_node(nodegroup_h_division_board(tag_support=kwargs.get('tag_support', False)).name,
+ input_kwargs={'cell_size': cell_size, 'horizontal_cell_num': horizontal_cell_num,
+ 'division_board_thickness': division_board_thickness, 'depth': depth})
+
+ h_division_boards = []
+ for j in range(1, kwargs['vertical_cell_num']):
+ h_division_index = nw.new_node(Nodes.Integer, label='HDivisionIndex')
+ h_division_index.integer = j
+
+ hdivisionplacement = nw.new_node(nodegroup_h_division_placement().name,
+ input_kwargs={'depth': depth, 'cell_size': cell_size,
+ 'leg_height': base_leg_height,
+ 'division_board_thickness': external_board_thickness,
+ 'external_board_thickness': division_board_thickness,
+ 'index': h_division_index})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': hdivisionboard, 'Translation': hdivisionplacement})
+ h_division_boards.append(transform)
+
+ if len(h_division_boards) > 0:
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': h_division_boards})
+ all_components.append(join_geometry)
+
+ if kwargs['has_backboard']:
+ backboard = nw.new_node(nodegroup_back_board().name,
+ input_kwargs={'X': topbottomboard.outputs["x"], 'Z': vdivisionboard.outputs["Value"],
+ 'leg': base_leg_height, 'external': external_board_thickness})
+ all_components.append(backboard)
+ else:
+ attach_square_size = nw.new_node(Nodes.Value, label='attach_square_size')
+ attach_square_size.outputs[0].default_value = kwargs['attachment_size']
+
+ attachgadget = nw.new_node(nodegroup_attach_gadget().name,
+ input_kwargs={'z': vdivisionboard.outputs["Value"], 'base_leg': base_leg_height,
+ 'x': topbottomboard.outputs["x"],
+ 'thickness': external_board_thickness,
+ 'size': attach_square_size})
+ all_components.append(attachgadget)
+
+ join_geometry_4 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': all_components})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_4})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': realize_instances,
+ 'Material': surface.shaderfunc_to_material(kwargs['wood_material'])})
+
+ base_leg_size = nw.new_node(Nodes.Value, label='base_leg_size')
+ base_leg_size.outputs[0].default_value = kwargs['base_leg_size']
+
+ merge_components = [set_material_1]
+ if kwargs['has_base_frame']:
+ baseframe = nw.new_node(nodegroup_base_frame().name,
+ input_kwargs={'leg_height': base_leg_height, 'leg_size': base_leg_size, 'depth': depth,
+ 'bottom_x': topbottomboard.outputs["x"]})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': baseframe})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': realize_instances_1,
+ 'Material': surface.shaderfunc_to_material(kwargs['base_material'])})
+ merge_components.append(set_material)
+
+ screwhead = nw.new_node(nodegroup_screw_head().name,
+ input_kwargs={'Z': vdivisionboard.outputs["Value"], 'leg': base_leg_height,
+ 'X': topbottomboard.outputs["x"], 'external': external_board_thickness,
+ 'depth': depth})
+
+ realize_instances_2 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': screwhead})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': realize_instances_2,
+ 'Material': surface.shaderfunc_to_material(metal.get_shader())})
+ merge_components.append(set_material_2)
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': merge_components})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': join_geometry_2})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+class CellShelfBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(CellShelfBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ with FixedSeed(factory_seed):
+ self.params = self.sample_params()
+ self.params = self.get_asset_params(self.params)
+
+ def get_asset_params(self, params):
+
+ if params is None:
+ params = {}
+
+ if params.get('depth', None) is None:
+ params['depth'] = np.clip(normal(0.39, 0.05), 0.29, 0.49)
+ if params.get('cell_size', None) is None:
+ params['cell_size'] = np.clip(normal(0.335, 0.03), 0.26, 0.40)
+ if params.get('vertical_cell_num', None) is None:
+ params['vertical_cell_num'] = randint(1, 7)
+ if params.get('horizontal_cell_num', None) is None:
+ params['horizontal_cell_num'] = randint(1, 7)
+ if params.get('division_board_thickness', None) is None:
+ params['division_board_thickness'] = np.clip(normal(0.015, 0.005), 0.008, 0.022)
+ if params.get('external_board_thickness', None) is None:
+ params['external_board_thickness'] = np.clip(normal(0.04, 0.005), 0.028, 0.052)
+ if params.get('has_backboard', None) is None:
+ params['has_backboard'] = False
+ if params.get('has_base_frame', None) is None:
+ params['has_base_frame'] = np.random.choice([True, False], p=[0.4, 0.6])
+ if params['has_base_frame']:
+ if params.get('base_leg_height', None) is None:
+ params['base_leg_height'] = np.clip(normal(0.174, 0.03), 0.1, 0.25)
+ if params.get('base_leg_size', None) is None:
+ params['base_leg_size'] = np.clip(normal(0.035, 0.007), 0.02, 0.05)
+ if params.get('base_material', None) is None:
+ params['base_material'] = np.random.choice(['black', 'white'], p=[0.4, 0.6])
+ else:
+ params['base_leg_height'] = 0.0
+ params['base_leg_size'] = 0.0
+ params['base_material'] = 'white'
+ if params.get('attachment_size', None) is None:
+ params['attachment_size'] = np.clip(normal(0.05, 0.02), 0.02, 0.1)
+ if params.get('wood_material', None) is None:
+ params['wood_material'] = np.random.choice(['black_wood', 'white', 'wood'], p=[0.3, 0.2, 0.5])
+ params['tag_support'] = True
+ params = self.get_material_func(params, randomness=True)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ if params['wood_material'] == 'white':
+ if randomness:
+ params['wood_material'] = lambda x: shader_shelves_white(x, **shader_shelves_white_sampler())
+ else:
+ params['wood_material'] = shader_shelves_white
+ elif params['wood_material'] == 'black_wood':
+ if randomness:
+ params['wood_material'] = lambda x: shader_shelves_black_wood(x, **shader_shelves_black_wood_sampler())
+ else:
+ params['wood_material'] = shader_shelves_black_wood
+ elif params['wood_material'] == 'wood':
+ if randomness:
+ params['wood_material'] = lambda x: shader_shelves_wood(x, **shader_shelves_wood_sampler())
+ else:
+ params['wood_material'] = shader_shelves_wood
+ else:
+ raise NotImplementedError
+
+ if params['base_material'] == 'white':
+ if randomness:
+ params['base_material'] = lambda x: shader_shelves_white_metallic(x, **shader_shelves_white_metallic_sampler())
+ else:
+ params['base_material'] = shader_shelves_white_metallic
+ elif params['base_material'] == 'black':
+ if randomness:
+ params['base_material'] = lambda x: shader_shelves_black_metallic(x, **shader_shelves_black_metallic_sampler())
+ else:
+ params['base_material'] = shader_shelves_black_metallic
+ else:
+ raise NotImplementedError
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.params
+ surface.add_geomod(obj, geometry_nodes, attributes=[], input_kwargs=obj_params, apply=True)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class CellShelfFactory(CellShelfBaseFactory):
+
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (uniform(0.3, 0.45),
+ uniform(2 * 0.35, 6 * 0.35),
+ uniform(1 * 0.35, 6 * 0.35))
+ h_cell_num = int(params['Dimensions'][1] / 0.35)
+ params['cell_size'] = params['Dimensions'][1] / h_cell_num
+ params['horizontal_cell_num'] = h_cell_num
+ params['vertical_cell_num'] = max(int(params['Dimensions'][2] / params['cell_size']), 1)
+ params['depth'] = params['Dimensions'][0]
+ params['has_base_frame'] = False
+ params['Dimensions'] = list(params['Dimensions'])
+ params['Dimensions'][2] = params['vertical_cell_num'] * params['cell_size']
+ return params
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ x,y,z = self.params['Dimensions'][0], self.params['Dimensions'][1], self.params['Dimensions'][2]
+ return new_bbox(0, x, -y/2 * 1.1, y/2 * 1.1, 0, z + (self.params['vertical_cell_num'] - 1) * self.params['division_board_thickness'] + 2 * self.params['external_board_thickness'] )
+
+class TVStandFactory(CellShelfFactory):
+
+ def sample_params(self): # TODO HACK copied code just following the pattern to get this working
+ params = dict()
+ params['Dimensions'] = (
+ uniform(0.3, 0.45),
+ uniform(2 * 0.35, 6 * 0.35),
+ uniform(0.3, 0.5)
+ )
+ h_cell_num = int(params['Dimensions'][1] / 0.35)
+ params['cell_size'] = params['Dimensions'][1] / h_cell_num
+ params['horizontal_cell_num'] = h_cell_num
+ params['vertical_cell_num'] = max(int(params['Dimensions'][2] / params['cell_size']), 1)
+ params['depth'] = params['Dimensions'][0]
+ params['has_base_frame'] = False
+ params['Dimensions'] = list(params['Dimensions'])
+ params['Dimensions'][2] = params['vertical_cell_num'] * params['cell_size']
+ return params
\ No newline at end of file
diff --git a/infinigen/assets/shelves/countertop.py b/infinigen/assets/shelves/countertop.py
new file mode 100644
index 000000000..087503dce
--- /dev/null
+++ b/infinigen/assets/shelves/countertop.py
@@ -0,0 +1,154 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+import shapely
+from numpy.random import uniform
+
+from infinigen.assets.materials import marble, ceramic
+from infinigen.assets.materials.woods import wood_tile
+from infinigen.assets.utils.decorate import read_normal, read_center, select_faces
+from infinigen.assets.utils.mesh import separate_selected, snap_mesh
+from infinigen.assets.utils.object import join_objects
+from infinigen.assets.utils.shapes import obj2polygon, safe_polygon2obj, buffer, dissolve_limited
+from infinigen.core.placement.factory import AssetFactory, make_asset_collection
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.random import random_general as rg
+
+from infinigen.core.util import blender as butil
+
+
+class CountertopFactory(AssetFactory):
+ surfaces = 'weighted_choice', (5, marble), (2, ceramic), (2, wood_tile)
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ self.surface = rg(self.surfaces)
+ self.thickness = uniform(.02, .06)
+ self.extrusion = 0 if uniform() < .4 else uniform(.02, .03)
+ self.h_snap = .5
+ self.v_snap = .5
+ self.v_merge = .1
+ self.z_range = .5, 1.5
+ self.surface = rg(self.surfaces)
+
+ @staticmethod
+ def generate_shelves():
+ from .kitchen_cabinet import KitchenCabinetFactory
+ from .simple_desk import SimpleDeskFactory
+ shelves = make_asset_collection(
+ [
+ KitchenCabinetFactory(np.random.randint(1e7)),
+ SimpleDeskFactory(np.random.randint(1e7))], 10
+ )
+ for s in shelves.objects:
+ s.location = *uniform(-1, 1, 2), uniform(0, .5)
+ s.rotation_euler[-1] = np.pi / 2 * np.random.randint(4)
+ return shelves
+
+ def create_asset(self, shelves=None, **params) -> bpy.types.Object:
+ if shelves is None:
+ shelves_generated = True
+ shelves = self.generate_shelves()
+ else:
+ shelves_generated = False
+ geoms, zs = [], []
+ for s in shelves.objects:
+ t = deep_clone_obj(s)
+ z = read_center(t)[:, -1]
+ max_z = np.max(z[(self.z_range[0] < z) & (z < self.z_range[1])])
+ selection = (read_normal(t)[:, -1] > .5) & (z - 1e-2 < max_z) & (max_z < z + 1e-2)
+ select_faces(t, selection)
+ r = separate_selected(t, True)
+ r.location = s.location
+ r.rotation_euler = s.rotation_euler
+ butil.apply_transform(r, True)
+ p = self.rebuffer(obj2polygon(r), self.h_snap)
+ q = buffer(p, self.extrusion)
+ geoms.append(q)
+ zs.append(max_z + s.location[-1])
+ butil.delete([r, t])
+ indices = np.argsort(zs)
+ geoms_ = [geoms[i] for i in indices]
+ zs_ = [zs[i] for i in indices]
+ geoms, zs = [], []
+ for i in range(len(indices)):
+ if i == 0:
+ geoms.append(geoms_[i])
+ zs.append(zs_[i])
+ elif zs_[i] < zs[-1] + self.v_merge:
+ geoms[-1] = self.rebuffer(geoms[-1].union(geoms_[i]), self.h_snap)
+ else:
+ geoms.append(geoms_[i])
+ zs.append(zs_[i])
+ groups = []
+ for i in range(len(geoms)):
+ for j in range(i):
+ if geoms[i].distance(geoms[j]) <= self.h_snap and zs[i] - zs[j] < self.v_snap:
+ group = next(g for g in groups if j in g)
+ group.add(i)
+ break
+ else:
+ groups.append({i})
+ objs = []
+ for group in groups:
+ n = len(group)
+ geoms_ = [geoms[i] for i in group]
+ zs_ = [zs[i] for i in group]
+ geom_unions = [self.rebuffer(shapely.union_all(geoms_[i:]), self.h_snap / 2) for i in
+ range(n)]
+ geom_unions.append(shapely.Point())
+ shapes = [self.rebuffer(geom_unions[i].difference(geom_unions[i + 1]), -1e-4) for i in range(n)]
+ for s, z in zip(shapes, zs_):
+ if s.area > 0:
+ o = safe_polygon2obj(self.rebuffer(s, -1e-4).buffer(0))
+ if o is not None:
+ o.location[-1] = z
+ butil.apply_transform(o, True)
+ objs.append(o)
+ ss = []
+ for i in range(n - 1, -1, -1):
+ for j in range(i - 1, -1, -1):
+ s = buffer(shapes[i], 1e-4).intersection(buffer(shapes[j], 1e-4))
+ ss.append(s)
+ for c in ss[:-1]:
+ s = s.difference(buffer(c, 1e-4))
+ if s.area == 0:
+ continue
+ o = safe_polygon2obj(s)
+ if o is None:
+ continue
+ butil.modify_mesh(o, 'WELD', merge_threshold=5e-4)
+ o.location[-1] = zs_[i]
+ with butil.ViewportMode(o, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, zs_[j] - zs_[i])}
+ )
+ objs.append(o)
+ obj = join_objects(objs)
+ snap_mesh(obj, 2e-2)
+ dissolve_limited(obj)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+ butil.modify_mesh(
+ obj, 'SOLIDIFY', thickness=self.thickness, use_even_offset=True, offset=1, use_quality_normals=True
+ )
+
+ if shelves_generated:
+ for s in shelves.objects:
+ s.parent = obj
+ return objs[0]
+
+ @staticmethod
+ def rebuffer(shape, distance):
+ return shape.buffer(distance, join_style='mitre', cap_style='flat').buffer(
+ -distance, join_style='mitre', cap_style='flat'
+ )
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py
new file mode 100644
index 000000000..78dc111c6
--- /dev/null
+++ b/infinigen/assets/shelves/doors.py
@@ -0,0 +1,739 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+import bpy
+
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_wood, shader_shelves_wood_sampler,
+ shader_glass)
+
+@node_utils.to_nodegroup('nodegroup_node_group', singleton=False, type='GeometryNodeTree')
+def nodegroup_node_group(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0120, 0.00060, 0.0400)})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Vertices': 64, 'Radius': 0.0100, 'Depth': 0.00050})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': (0.0050, 0.0000, 0.0000),
+ 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0200, 0.0006, 0.0120)})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cube_1, 'Translation': (0.0080, 0.0000, 0.0000)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [cube, transform, transform_1]})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'attach_height', 0.1000),
+ ('NodeSocketFloat', 'door_width', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["door_width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0181}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Z': group_input.outputs["attach_height"]})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_knob_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_knob_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 0.0100),
+ ('NodeSocketFloat', 'thickness_1', 0.5000),
+ ('NodeSocketFloat', 'thickness_2', 0.5000),
+ ('NodeSocketFloat', 'length', 0.5000),
+ ('NodeSocketFloat', 'knob_mid_height', 0.0000),
+ ('NodeSocketFloat', 'edge_width', 0.5000),
+ ('NodeSocketFloat', 'door_width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["thickness_2"], 1: group_input.outputs["thickness_1"]})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: group_input.outputs["length"]})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': group_input.outputs["Radius"], 'Depth': add_1})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["door_width"], 1: group_input.outputs["edge_width"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.005})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': add_2, 'Y': multiply_1, 'Z': group_input.outputs["knob_mid_height"]})
+
+ transform_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_6,
+ 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_6},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_mid_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_mid_board(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"]}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_k = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_k = nw.new_node(Nodes.Math, input_kwargs={0: multiply_k, 1: 0.004})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.0001})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_3, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_1})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_4})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_4,
+ 'Material': surface.shaderfunc_to_material(kwargs['material'][0])})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_7, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 1.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_2})
+
+ transform_7 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_8})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_7,
+ 'Material': surface.shaderfunc_to_material(kwargs['material'][1])})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': realize_instances, 'mid_height': multiply},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_mid_board_001', singleton=False, type='GeometryNodeTree')
+def nodegroup_mid_board_001(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ multiply_k = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_k = nw.new_node(Nodes.Math, input_kwargs={0: multiply_k, 1: 0.004})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -0.0001})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_3, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_k, 'Z': multiply_1})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_4})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': transform_4,
+ 'Material': surface.shaderfunc_to_material(kwargs['material'][0])})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': set_material})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': realize_instances, 'mid_height': multiply},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_double_rampled_edge', singleton=False, type='GeometryNodeTree')
+def nodegroup_double_rampled_edge(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness_2', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'thickness_1', 0.5000),
+ ('NodeSocketFloat', 'ramp_angle', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_10})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 3, 'Radius': 0.0100})
+
+ endpoint_selection = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ramp_angle"], 1: 0.0000})
+
+ tangent = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'TANGENT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_2"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: tangent, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: 2.0000, 1: multiply}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_1"], 1: 0.0000})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': add_4})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Selection': endpoint_selection,
+ 'Position': combine_xyz_7})
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: add_3})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': add_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position, 'Selection': endpoint_selection_1,
+ 'Position': combine_xyz_8})
+
+ index = nw.new_node(Nodes.Index)
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_THAN'})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 0.9900}, attrs={'operation': 'GREATER_THAN'})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: less_than, 1: greater_than})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Y': add_4})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position_1, 'Selection': op_and,
+ 'Position': combine_xyz_9})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line, 'Profile Curve': set_position_2, 'Fill Caps': True})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': add_4, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_6})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': add_3, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_7})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_6})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_8})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_11})
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_12})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': set_position_2, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line_1, 'Profile Curve': transform_2, 'Fill Caps': True})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_mesh, transform_4, curve_to_mesh_1]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance,
+ input_kwargs={'Geometry': join_geometry_1, 'Distance': 0.0001})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': merge_by_distance})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': realize_instances, 'Level': 4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': subdivide_mesh},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_ramped_edge', singleton=False, type='GeometryNodeTree')
+def nodegroup_ramped_edge(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness_2', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'thickness_1', 0.5000),
+ ('NodeSocketFloat', 'ramp_angle', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_10})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 3, 'Radius': 0.0100})
+
+ endpoint_selection = nw.new_node(Nodes.EndpointSelection, input_kwargs={'End Size': 0})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ramp_angle"], 1: 0.0000})
+
+ tangent = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'TANGENT'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_2"], 1: 0.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: tangent, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: subtract}, attrs={'operation': 'SUBTRACT'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness_1"], 1: 0.0000})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': add_4})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Selection': endpoint_selection,
+ 'Position': combine_xyz_7})
+
+ endpoint_selection_1 = nw.new_node(Nodes.EndpointSelection, input_kwargs={'Start Size': 0})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: add_3})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': add_5})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position, 'Selection': endpoint_selection_1,
+ 'Position': combine_xyz_8})
+
+ index = nw.new_node(Nodes.Index)
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_THAN'})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 0.9900}, attrs={'operation': 'GREATER_THAN'})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: less_than, 1: greater_than})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Y': add_4})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': set_position_1, 'Selection': op_and,
+ 'Position': combine_xyz_9})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line, 'Profile Curve': set_position_2, 'Fill Caps': True})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Y': add_4, 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': add_3, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_1})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_4, 1: multiply_5})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_4, 'Y': add_6})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_11 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_6})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_11})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh, transform_4]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance,
+ input_kwargs={'Geometry': join_geometry_1, 'Distance': 0.0001})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': merge_by_distance})
+
+ subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': realize_instances, 'Level': 4})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_7})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': subdivide_mesh, 'Translation': combine_xyz_4})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_panel_edge_frame', singleton=False, type='GeometryNodeTree')
+def nodegroup_panel_edge_frame(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'vertical_edge', None),
+ ('NodeSocketFloat', 'door_width', 0.5000),
+ ('NodeSocketFloat', 'door_height', 0.0000),
+ ('NodeSocketGeometry', 'horizontal_edge', None)])
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["door_width"], 2: 0.0010},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ transform_7 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': group_input.outputs["horizontal_edge"],
+ 'Translation': (0.0000, -0.0001, 0.0000),
+ 'Scale': (0.9999, 1.0000, 1.0000)})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: 1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -0.0001})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["door_height"], 1: 0.0001})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': add_1})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_7, 'Translation': combine_xyz_2,
+ 'Rotation': (0.0000, -1.5708, 0.0000)})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0001})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_7, 'Translation': combine_xyz_1,
+ 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': group_input.outputs["vertical_edge"], 'Translation': combine_xyz})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ # transform_1 = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': transform_1})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_3, transform_2, transform_1, transform]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Value': multiply, 'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+def geometry_door_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ door_height = nw.new_node(Nodes.Value, label='door_height')
+ door_height.outputs[0].default_value = kwargs['door_height']
+
+ door_edge_thickness_2 = nw.new_node(Nodes.Value, label='door_edge_thickness_2')
+ door_edge_thickness_2.outputs[0].default_value = kwargs['edge_thickness_2']
+
+ door_edge_width = nw.new_node(Nodes.Value, label='door_edge_width')
+ door_edge_width.outputs[0].default_value = kwargs['edge_width']
+
+ door_edge_thickness_1 = nw.new_node(Nodes.Value, label='door_edge_thickness_1')
+ door_edge_thickness_1.outputs[0].default_value = kwargs['edge_thickness_1']
+
+ door_edge_ramp_angle = nw.new_node(Nodes.Value, label='door_edge_ramp_angle')
+ door_edge_ramp_angle.outputs[0].default_value = kwargs['edge_ramp_angle']
+
+ ramped_edge = nw.new_node(nodegroup_ramped_edge().name,
+ input_kwargs={'height': door_height, 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width, 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle})
+
+ door_width = nw.new_node(Nodes.Value, label='door_width')
+ door_width.outputs[0].default_value = kwargs['door_width']
+
+ ramped_edge_1 = nw.new_node(nodegroup_ramped_edge().name,
+ input_kwargs={'height': door_width, 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width, 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle})
+
+ panel_edge_frame = nw.new_node(nodegroup_panel_edge_frame().name,
+ input_kwargs={'vertical_edge': ramped_edge, 'door_width': door_width,
+ 'door_height': door_height, 'horizontal_edge': ramped_edge_1})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: panel_edge_frame.outputs["Value"], 1: 0.0001})
+
+ mid_board_thickness = nw.new_node(Nodes.Value, label='mid_board_thickness')
+ mid_board_thickness.outputs[0].default_value = kwargs['board_thickness']
+
+ if kwargs['has_mid_ramp']:
+ mid_board = nw.new_node(nodegroup_mid_board(material=kwargs['panel_material']).name,
+ input_kwargs={'height': door_height, 'thickness': mid_board_thickness,
+ 'width': door_width})
+ else:
+ mid_board = nw.new_node(nodegroup_mid_board_001(material=kwargs['panel_material']).name,
+ input_kwargs={'height': door_height, 'thickness': mid_board_thickness,
+ 'width': door_width})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': add, 'Y': -0.0001, 'Z': mid_board.outputs["mid_height"]})
+
+ frame = [panel_edge_frame.outputs["Geometry"]]
+ if kwargs['has_mid_ramp']:
+ double_rampled_edge = nw.new_node(nodegroup_double_rampled_edge().name,
+ input_kwargs={'height': door_width, 'thickness_2': door_edge_thickness_2,
+ 'width': door_edge_width, 'thickness_1': door_edge_thickness_1,
+ 'ramp_angle': door_edge_ramp_angle})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': double_rampled_edge, 'Translation': combine_xyz_5,
+ 'Rotation': (0.0000, 1.5708, 0.0000)})
+ frame.append(transform_5)
+
+ knob_raduis = nw.new_node(Nodes.Value, label='knob_raduis')
+ knob_raduis.outputs[0].default_value = kwargs['knob_R']
+
+ know_length = nw.new_node(Nodes.Value, label='know_length')
+ know_length.outputs[0].default_value = kwargs['knob_length']
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: door_height}, attrs={'operation': 'MULTIPLY'})
+
+ knob_handle = nw.new_node(nodegroup_knob_handle().name,
+ input_kwargs={'Radius': knob_raduis, 'thickness_1': door_edge_thickness_1,
+ 'thickness_2': door_edge_thickness_2, 'length': know_length,
+ 'knob_mid_height': multiply,
+ 'edge_width': door_edge_width, 'door_width': door_width})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': frame + [knob_handle]})
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_1,
+ 'Material': surface.shaderfunc_to_material(kwargs['frame_material'])})
+
+ geos = [set_material_3, mid_board.outputs["Geometry"]]
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': geos})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: door_width, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': transform})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances_1})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate,
+ 'Scale': (-1.0 if kwargs['door_left_hinge'] else 1.0, 1.0000, 1.0000)})
+
+ if kwargs['door_left_hinge']:
+ transform_1 = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': transform_1})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_1, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_2},
+ attrs={'is_active_output': True})
+
+
+class CabinetDoorBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(CabinetDoorBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {}
+
+ def get_asset_params(self, i=0):
+ params = self.params.copy()
+ if params.get('door_height', None) is None:
+ params['door_height'] = uniform(0.7, 2.2)
+ if params.get('door_width', None) is None:
+ params['door_width'] = uniform(0.3, 0.4)
+ if params.get('edge_thickness_1', None) is None:
+ params['edge_thickness_1'] = uniform(0.01, 0.018)
+ if params.get('edge_width', None) is None:
+ params['edge_width'] = uniform(0.03, 0.05)
+ if params.get('edge_thickness_2', None) is None:
+ params['edge_thickness_2'] = uniform(0.005, 0.008)
+ if params.get('edge_ramp_angle', None) is None:
+ params['edge_ramp_angle'] = uniform(0.6, 0.8)
+ params['board_thickness'] = params['edge_thickness_1'] - 0.005
+ if params.get('knob_R', None) is None:
+ params['knob_R'] = uniform(0.003, 0.006)
+ if params.get('knob_length', None) is None:
+ params['knob_length'] = uniform(0.018, 0.035)
+ if params.get('attach_height', None) is None:
+ gap = uniform(0.05, 0.15)
+ params['attach_height'] = [gap, params['door_height'] - gap]
+ if params.get('has_mid_ramp', None) is None:
+ params['has_mid_ramp'] = np.random.choice([True, False], p=[0.6, 0.4])
+ if params.get('door_left_hinge', None) is None:
+ params['door_left_hinge'] = False
+
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.5, 0.2, 0.3])
+ if params.get('panel_material', None) is None:
+ if params['has_mid_ramp']:
+ lower_mat = np.random.choice([params['frame_material'], 'glass'], p=[0.7, 0.3])
+ upper_mat = np.random.choice([lower_mat, 'glass'], p=[0.6, 0.4])
+ params['panel_material'] = [lower_mat, upper_mat]
+ else:
+ params['panel_material'] = [params['frame_material']]
+
+ params = self.get_material_func(params)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ white_wood_params = shader_shelves_white_sampler()
+ black_wood_params = shader_shelves_black_wood_sampler()
+ normal_wood_params = shader_shelves_wood_sampler()
+ if params['frame_material'] == 'white':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['frame_material'] = shader_shelves_white
+ elif params['frame_material'] == 'black_wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, z_axis_texture=True)
+ elif params['frame_material'] == 'wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, z_axis_texture=True)
+
+ materials = []
+ if not isinstance(params['panel_material'], list):
+ params['panel_material'] = [params['board_material']]
+ for mat in params['panel_material']:
+ if mat == 'white':
+ if randomness:
+ mat = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ mat = shader_shelves_white
+ elif mat == 'black_wood':
+ if randomness:
+ mat = lambda x: shader_shelves_black_wood(x, **black_wood_params, z_axis_texture=True)
+ else:
+ mat = lambda x: shader_shelves_black_wood(x, z_axis_texture=True)
+ elif mat == 'wood':
+ if randomness:
+ mat = lambda x: shader_shelves_wood(x, **normal_wood_params, z_axis_texture=True)
+ else:
+ mat = lambda x: shader_shelves_wood(x, z_axis_texture=True)
+ elif mat == 'glass':
+ if randomness:
+ mat = lambda x: shader_glass(x)
+ else:
+ mat = shader_glass
+ materials.append(mat)
+ params['panel_material'] = materials
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_door_nodes, apply=True, attributes=[], input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ if params.get('ret_params', False):
+ return obj, obj_params
+
+ return obj
+
diff --git a/infinigen/assets/shelves/drawers.py b/infinigen/assets/shelves/drawers.py
new file mode 100644
index 000000000..7393f9819
--- /dev/null
+++ b/infinigen/assets/shelves/drawers.py
@@ -0,0 +1,416 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials import metal
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core.tagging import tag_object, tag_nodegroup
+
+import bpy
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_wood, shader_shelves_wood_sampler,
+ shader_glass)
+
+
+@node_utils.to_nodegroup('nodegroup_board_rail', singleton=False, type='GeometryNodeTree')
+def nodegroup_board_rail(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ cylinder_1 = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': 0.0040, 'Depth': 0.0050})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder_1.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cylinder_1.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0200})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_1})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_3,
+ 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 0.0300}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': 0.0020, 'Y': subtract, 'Z': group_input.outputs["width"]})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': 0.0030, 'Depth': subtract})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_1,
+ 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_1, 'Scale': (1.0000, 1.0000, -1.0000)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_2, transform_1]})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_5, transform, join_geometry_2]})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: 0.0030})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: 0.0200})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_2, 'Y': multiply_3, 'Z': add_3})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Translation': combine_xyz_2})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_3, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_4, transform_3]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_3},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_kallax_drawer_frame', singleton=False, type='GeometryNodeTree')
+def nodegroup_kallax_drawer_frame(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 4, 'Vertices Y': 4, 'Vertices Z': 4})
+
+ store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -0.0001})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 2: 0.0100}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': add_4, 'Z': multiply_add})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -0.0001})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: add_5})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_6, 'Y': add_1, 'Z': add})
+
+ cube_1 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_2, 'Vertices X': 4, 'Vertices Y': 4, 'Vertices Z': 4})
+
+ store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_1.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube_1.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply_add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -0.5000, 2: -0.0001},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_add_1, 'Z': 0.0100})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_3})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_3, 'Y': add, 'Z': add_2})
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': 4, 'Vertices Y': 4, 'Vertices Z': 4})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube_2.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube_2.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -1.0000, 2: multiply_2},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 2: 0.0100}, attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_add_2, 'Z': multiply_add_3})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_5})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_1, transform, transform_2, transform_3]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_door_knob', singleton=False, type='GeometryNodeTree')
+def nodegroup_door_knob(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 0.0040),
+ ('NodeSocketFloat', 'length', 0.5000),
+ ('NodeSocketFloat', 'z', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["length"], 1: 0.0000})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 64, 'Radius': group_input.outputs["Radius"], 'Depth': add})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0001})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["z"], 1: 0.0000})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_1, 'Z': multiply_1})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_2,
+ 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_drawer_door_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_drawer_door_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cube.outputs["Mesh"], 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply, 'Z': multiply_1})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ door_thickness = nw.new_node(Nodes.Value, label='door_thickness')
+ door_thickness.outputs[0].default_value = kwargs['drawer_board_thickness']
+
+ drawer_board_width = nw.new_node(Nodes.Value, label='drawer_board_width')
+ drawer_board_width.outputs[0].default_value = kwargs['drawer_board_width']
+
+ drawer_board_height = nw.new_node(Nodes.Value, label='drawer_board_height')
+ drawer_board_height.outputs[0].default_value = kwargs['drawer_board_height']
+
+ drawer_door_board = nw.new_node(nodegroup_drawer_door_board().name,
+ input_kwargs={'thickness': door_thickness, 'width': drawer_board_width,
+ 'height': drawer_board_height})
+
+ knob_radius = nw.new_node(Nodes.Value, label='knob_radius')
+ knob_radius.outputs[0].default_value = kwargs['knob_radius']
+
+ knob_length = nw.new_node(Nodes.Value, label='knob_length')
+ knob_length.outputs[0].default_value = kwargs['knob_length']
+
+ door_knob = nw.new_node(nodegroup_door_knob().name,
+ input_kwargs={'Radius': knob_radius, 'length': knob_length, 'z': drawer_board_height})
+
+ drawer_depth = nw.new_node(Nodes.Value, label='drawer_depth')
+ drawer_depth.outputs[0].default_value = kwargs['drawer_depth'] - kwargs['drawer_board_thickness']
+
+ drawer_side_height = nw.new_node(Nodes.Value, label='drawer_side_height')
+ drawer_side_height.outputs[0].default_value = kwargs['drawer_side_height']
+
+ drawer_width = nw.new_node(Nodes.Value, label='drawer_width')
+ drawer_width.outputs[0].default_value = kwargs['drawer_width']
+
+ kallax_drawer_frame = nw.new_node(nodegroup_kallax_drawer_frame().name,
+ input_kwargs={'depth': drawer_depth, 'height': drawer_side_height,
+ 'thickness': door_thickness, 'width': drawer_width})
+
+ side_tilt_width = nw.new_node(Nodes.Value, label='side_tilt_width')
+ side_tilt_width.outputs[0].default_value = kwargs['side_tilt_width']
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={
+ 'Geometry': [door_knob, drawer_door_board, kallax_drawer_frame]})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry,
+ 'Material': surface.shaderfunc_to_material(kwargs['frame_material'])})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': set_material_2})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output_1 = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+class CabinetDrawerBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(CabinetDrawerBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {}
+
+ def get_asset_params(self, i=0):
+ params = self.params.copy()
+ if params.get('drawer_board_thickness', None) is None:
+ params['drawer_board_thickness'] = uniform(0.005, 0.01)
+ if params.get('drawer_board_width', None) is None:
+ params['drawer_board_width'] = uniform(0.3, 0.7)
+ if params.get('drawer_board_height', None) is None:
+ params['drawer_board_height'] = uniform(0.25, 0.4)
+ if params.get('drawer_depth', None) is None:
+ params['drawer_depth'] = uniform(0.3, 0.4)
+ if params.get('drawer_side_height', None) is None:
+ params['drawer_side_height'] = uniform(0.05, 0.2)
+ if params.get('drawer_width', None) is None:
+ params['drawer_width'] = params['drawer_board_width'] - uniform(0.015, 0.025)
+ if params.get('side_tilt_width', None) is None:
+ params['side_tilt_width'] = uniform(0.02, 0.03)
+ if params.get('knob_radius', None) is None:
+ params['knob_radius'] = uniform(0.003, 0.006)
+ if params.get('knob_length', None) is None:
+ params['knob_length'] = uniform(0.018, 0.035)
+
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.5, 0.2, 0.3])
+ if params.get('knob_material', None) is None:
+ params['knob_material'] = np.random.choice([params['frame_material'], 'metal'], p=[0.5, 0.5])
+
+ params = self.get_material_func(params)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ white_wood_params = shader_shelves_white_sampler()
+ black_wood_params = shader_shelves_black_wood_sampler()
+ normal_wood_params = shader_shelves_wood_sampler()
+ if params['frame_material'] == 'white':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['frame_material'] = shader_shelves_white
+ elif params['frame_material'] == 'black_wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, z_axis_texture=True)
+ elif params['frame_material'] == 'wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, z_axis_texture=True)
+
+ if params['knob_material'] == 'metal':
+ params['knob_material'] = metal.get_shader()
+ else:
+ params['knob_material'] = params['frame_material']
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, apply=True, attributes=[], input_kwargs=obj_params)
+
+ if params.get('ret_params', False):
+ return obj, obj_params
+
+ return obj
diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py
new file mode 100644
index 000000000..6dea292a0
--- /dev/null
+++ b/infinigen/assets/shelves/kitchen_cabinet.py
@@ -0,0 +1,328 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials.shelf_shaders import get_shelf_material
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+from infinigen.core.util.math import FixedSeed
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube, blender_rotate
+from infinigen.assets.shelves.large_shelf import LargeShelfBaseFactory, LargeShelfFactory
+from infinigen.assets.shelves.doors import CabinetDoorBaseFactory
+from infinigen.assets.shelves.drawers import CabinetDrawerBaseFactory
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_wood, shader_shelves_wood_sampler
+)
+from infinigen.assets.utils.object import new_bbox
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ cabinets = []
+ for i, component in enumerate(kwargs['components']):
+ frame_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': component[0]})
+
+ attachments = []
+ if component[1] == 'door':
+ right_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': component[2][0]})
+ left_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object':component[2][1]})
+
+ transform_r = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': right_door_info.outputs['Geometry'],
+ 'Translation': component[2][2]['door_hinge_pos'][0],
+ 'Rotation': (0, 0, component[2][2]['door_open_angle'])})
+ attachments.append(transform_r)
+ if len(component[2][2]['door_hinge_pos']) > 1:
+ transform_l = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': left_door_info.outputs['Geometry'],
+ 'Translation': component[2][2]['door_hinge_pos'][1],
+ 'Rotation': (0, 0, component[2][2]['door_open_angle'])})
+ attachments.append(transform_l)
+ elif component[1] == 'drawer':
+
+ for j, drawer in enumerate(component[2]):
+ drawer_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': drawer[0]})
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': drawer_info.outputs['Geometry'],
+ 'Translation': drawer[1]['drawer_hinge_pos']})
+ attachments.append(transform)
+ else:
+ continue
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': attachments})
+ #[frame_info.outputs['Geometry']]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry,
+ 'Translation': (0, kwargs['y_translations'][i], 0)})
+ cabinets.append(transform)
+
+ try:
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': cabinets})
+ except TypeError:
+ import pdb; pdb.set_trace()
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': join_geometry_1}, attrs={'is_active_output': True})
+
+
+class KitchenCabinetBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(KitchenCabinetBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.frame_params = {}
+ self.material_params = {}
+ self.cabinet_widths = []
+ self.frame_fac = LargeShelfBaseFactory(factory_seed)
+ self.door_fac = CabinetDoorBaseFactory(factory_seed)
+ self.drawer_fac = CabinetDrawerBaseFactory(factory_seed)
+ self.drawer_only = False
+ with FixedSeed(factory_seed):
+ self.params = self.sample_params()
+
+ def sample_params(self):
+ pass
+
+ def get_material_params(self):
+ with FixedSeed(self.factory_seed):
+ params = self.material_params.copy()
+ if params.get('frame_material', None) is None:
+ with FixedSeed(self.factory_seed):
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.4, 0.3, 0.3])
+ params['board_material'] = params['frame_material']
+ return self.get_material_func(params, randomness=True)
+
+ def get_material_func(self, params, randomness=True):
+ with FixedSeed(self.factory_seed):
+ white_wood_params = shader_shelves_white_sampler()
+ black_wood_params = shader_shelves_black_wood_sampler()
+ normal_wood_params = shader_shelves_wood_sampler()
+ if params['frame_material'] == 'white':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['frame_material'] = shader_shelves_white
+ elif params['frame_material'] == 'black_wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, z_axis_texture=True)
+ elif params['frame_material'] == 'wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, z_axis_texture=True)
+
+ if params['board_material'] == 'white':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['board_material'] = shader_shelves_white
+ elif params['board_material'] == 'black_wood':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params)
+ else:
+ params['board_material'] = shader_shelves_black_wood
+ elif params['board_material'] == 'wood':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params)
+ else:
+ params['board_material'] = shader_shelves_wood
+
+ params['panel_meterial'] = params['frame_material']
+ params['knob_material'] = params['frame_material']
+ return params
+
+ def get_frame_params(self, width, i=0):
+ params = self.frame_params.copy()
+ params['shelf_cell_width'] = [width]
+ params.update(self.material_params.copy())
+ return params
+
+ def get_attach_params(self, attach_type, i=0):
+ param_sets = []
+ if attach_type == 'none':
+ pass
+ elif attach_type == 'door':
+ params = dict()
+ shelf_width = self.frame_params['shelf_width'] + self.frame_params['side_board_thickness'] * 2
+ if shelf_width <= 0.6:
+ params['door_width'] = shelf_width
+ params['has_mid_ramp'] = False
+ params['edge_thickness_1'] = 0.01
+ params['door_hinge_pos'] = [(self.frame_params['shelf_depth'] / 2. + 0.0025, -shelf_width / 2.,
+ self.frame_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+ else:
+ params['door_width'] = shelf_width / 2. - 0.0005
+ params['has_mid_ramp'] = False
+ params['edge_thickness_1'] = 0.01
+ params['door_hinge_pos'] = [(self.frame_params['shelf_depth'] / 2. + 0.008, -shelf_width / 2.,
+ self.frame_params['bottom_board_height']),
+ (self.frame_params['shelf_depth'] / 2. + 0.008, shelf_width / 2.,
+ self.frame_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+
+ params['door_height'] = (self.frame_params['division_board_z_translation'][-1] -
+ self.frame_params['division_board_z_translation'][0] +
+ self.frame_params['division_board_thickness'])
+ params.update(self.material_params.copy())
+ param_sets.append(params)
+ elif attach_type == 'drawer':
+ for i, h in enumerate(self.frame_params['shelf_cell_height']):
+ params = dict()
+ drawer_h = (self.frame_params['division_board_z_translation'][i+1]
+ - self.frame_params['division_board_z_translation'][i]
+ - self.frame_params['division_board_thickness'])
+ drawer_depth = self.frame_params['shelf_depth']
+ params['drawer_board_width'] = self.frame_params['shelf_width']
+ params['drawer_board_height'] = drawer_h
+ params['drawer_depth'] = drawer_depth
+ params['drawer_hinge_pos'] = (self.frame_params['shelf_depth'] / 2., 0,
+ (self.frame_params['division_board_thickness'] / 2. +
+ self.frame_params['division_board_z_translation'][i]))
+ params.update(self.material_params.copy())
+ param_sets.append(params)
+ else:
+ raise NotImplementedError
+
+ return param_sets
+
+ def get_cabinet_params(self, i=0):
+ x_translations = []
+ accum_w, thickness = 0, self.frame_params.get('side_board_thickness', 0.005) # instructed by Beining
+ for w in self.cabinet_widths:
+ accum_w += thickness + w / 2.
+ x_translations.append(accum_w)
+ accum_w += thickness + w / 2. + 0.0005
+ return x_translations
+
+ def create_cabinet_components(self, i, drawer_only=False):
+ # update material params
+ self.material_params = self.get_material_params()
+
+ components = []
+ for k, w in enumerate(self.cabinet_widths):
+ # create frame
+ frame_params = self.get_frame_params(w, i=i)
+ self.frame_fac.params = frame_params
+ frame, frame_params = self.frame_fac.create_asset(i=i, ret_params=True)
+ frame.name = f'cabinet_frame_{k}'
+ self.frame_params = frame_params
+
+ # create attach
+ if drawer_only:
+ attach_type = np.random.choice(['drawer', 'door'], p=[0.5, 0.5])
+ else:
+ attach_type = np.random.choice(['drawer', 'door', 'none'], p=[0.4, 0.4, 0.2])
+
+ attach_params = self.get_attach_params(attach_type, i=i)
+ if attach_type == 'door':
+ self.door_fac.params = attach_params[0]
+ self.door_fac.params['door_left_hinge'] = False
+ right_door, door_obj_params = self.door_fac.create_asset(i=i, ret_params=True)
+ right_door.name = f'cabinet_right_door_{k}'
+ self.door_fac.params = door_obj_params
+ self.door_fac.params['door_left_hinge'] = True
+ left_door, _ = self.door_fac.create_asset(i=i, ret_params=True)
+ left_door.name = f'cabinet_left_door_{k}'
+ components.append([frame, 'door', [right_door, left_door, attach_params[0]]])
+
+ elif attach_type == 'drawer':
+ drawers = []
+ for j, p in enumerate(attach_params):
+ self.drawer_fac.params = p
+ drawer = self.drawer_fac.create_asset(i=i)
+ drawer.name = f'drawer_{k}_layer{j}'
+ drawers.append([drawer, p])
+ components.append([frame, 'drawer', drawers])
+
+ elif attach_type == 'none':
+ components.append([frame, 'none'])
+
+ else:
+ raise NotImplementedError
+
+ return components
+
+ def create_asset(self, i=0, **params):
+ components = self.create_cabinet_components(i=i, drawer_only=self.drawer_only)
+ cabinet_params = self.get_cabinet_params(i=i)
+ join_objs = []
+
+ contain_attach = False
+ for com in components:
+ if com[1] == 'none':
+ continue
+ else:
+ contain_attach = True
+
+ if contain_attach:
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+ surface.add_geomod(obj, geometry_nodes, attributes=[], input_kwargs={
+ 'components': components,
+ 'y_translations': cabinet_params
+ }, apply=True)
+
+ join_objs += [obj]
+
+ for i, c in enumerate(components):
+ if c[1] == 'door':
+ butil.delete(c[2][:-1])
+ elif c[1] == 'drawer':
+ butil.delete([x[0] for x in c[2]])
+ c[0].location = (0, cabinet_params[i], 0)
+ butil.apply_transform(c[0], loc=True)
+ join_objs.append(c[0])
+
+ #butil.delete(c[:1])
+ obj = butil.join_objects(join_objs)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class KitchenCabinetFactory(KitchenCabinetBaseFactory):
+ def __init__(self, factory_seed, params={}, coarse=False, dimensions=None, drawer_only=False):
+ self.dimensions = dimensions
+ super().__init__(factory_seed, params, coarse)
+ self.drawer_only = drawer_only
+
+ def sample_params(self):
+ params = dict()
+ if self.dimensions is None:
+ dimensions = (
+ uniform(0.25, 0.35),
+ uniform(1.0, 4.0),
+ uniform(0.5, 1.3))
+ self.dimensions = dimensions
+ else:
+ dimensions = self.dimensions
+ params['Dimensions'] = dimensions
+
+ params['bottom_board_height'] = 0.06
+ params['shelf_depth'] = params['Dimensions'][0] - 0.01
+ num_h = int((params['Dimensions'][2] - 0.06) / 0.3)
+ params['shelf_cell_height'] = [(params['Dimensions'][2] - 0.06) / num_h for _ in range(num_h)]
+
+ self.frame_params = params
+
+ n_cells= max(int(params['Dimensions'][1] / 0.45),1)
+ intervals = np.random.uniform(0.55, 1.0, size=(n_cells,))
+ intervals = intervals / intervals.sum() * params['Dimensions'][1]
+ self.cabinet_widths = intervals.tolist()
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ x,y,z = self.dimensions
+ return new_bbox(-x/2 * 1.2, x/2 * 1.2, 0, y * 1.1, 0, (z + 0.06) * 1.03)
diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py
new file mode 100644
index 000000000..66c65efdb
--- /dev/null
+++ b/infinigen/assets/shelves/kitchen_space.py
@@ -0,0 +1,220 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo, Stamatis Alexandropoulos
+
+import bpy
+import bpy
+import mathutils
+from mathutils import Vector
+from numpy.random import uniform, normal, randint, choice
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core import tagging, tags as t
+
+from infinigen.assets.utils.object import new_bbox
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.shelves.kitchen_cabinet import KitchenCabinetFactory
+from infinigen.assets.table_decorations.sink import SinkFactory
+from infinigen.assets.wall_decorations.range_hood import RangeHoodFactory
+
+from infinigen.core.util import blender as butil
+
+from infinigen.assets.tables.table_top import nodegroup_generate_table_top
+from infinigen.assets.materials.table_materials import shader_marble
+from infinigen.core.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS
+
+def nodegroup_tag_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ index = nw.new_node(Nodes.Index)
+
+ equal = nw.new_node(Nodes.Compare, input_kwargs={2: index, 3: 5}, attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+
+ cube = tagging.tag_nodegroup(nw, group_input.outputs['Geometry'], t.Subpart.SupportSurface, selection=equal)
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': cube}, attrs={'is_active_output': True})
+
+def geometry_nodes_add_cabinet_top(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 0.0500
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Max"]})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Min"]})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: separate_xyz.outputs["X"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: 1.4140}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: separate_xyz.outputs["Y"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: subtract}, attrs={'operation': 'DIVIDE'})
+
+ generatetabletop = nw.new_node(nodegroup_generate_table_top().name,
+ input_kwargs={'Thickness': value, 'N-gon': 4, 'Profile Width': multiply, 'Aspect Ratio': divide, 'Fillet Ratio': 0.0100, 'Fillet Radius Vertical': 0.0100})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': generatetabletop, 'Material': surface.shaderfunc_to_material(shader_marble)})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Y"], 1: separate_xyz_1.outputs["Y"]})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 2.0000}, attrs={'operation': 'DIVIDE'})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Max"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': divide_1, 'Z': separate_xyz_2.outputs["Z"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': set_material, 'Translation': combine_xyz})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [group_input.outputs["Geometry"], transform_geometry]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+def geometry_node_to_tagged_bbox(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': bounding_box, 'Scale': (0.9700, 0.9700, 1.000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry}, attrs={'is_active_output': True})
+
+def geometry_node_to_bbox(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': bounding_box, 'Scale': (0.9700, 0.9700, 1.000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry}, attrs={'is_active_output': True})
+
+class KitchenSpaceFactory(AssetFactory):
+ def __init__(
+ self,
+ factory_seed,
+ coarse=False,
+ dimensions=None,
+ island=False
+ ):
+ super(KitchenSpaceFactory, self).__init__(factory_seed, coarse=coarse)
+
+ with FixedSeed(factory_seed):
+
+ if dimensions is None:
+ dimensions = Vector((
+ uniform(0.7, 1),
+ uniform(1.7, 5),
+ uniform(2.3, WALL_HEIGHT - WALL_THICKNESS)
+ ))
+
+ self.island = island
+ if self.island:
+ dimensions.x *= uniform(1.5, 2)
+
+ self.dimensions = dimensions
+
+ self.params = self.sample_parameters(dimensions)
+
+
+ def sample_parameters(self, dimensions):
+ self.cabinet_bottom_height = uniform(0.8, 1.0)
+ self.cabinet_top_height = uniform(0.8, 1.0)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ x, y, z = self.dimensions
+ box = new_bbox(-x/2 * 1.08, x/2 * 1.08, 0, y, 0, self.cabinet_bottom_height + 0.13)
+ surface.add_geomod(box, nodegroup_tag_cube, apply=True)
+
+ if not self.island:
+ box_top = new_bbox(-x/2, x*0.16, 0, y, z - self.cabinet_top_height - 0.1, z)
+ box = butil.join_objects([box, box_top])
+
+ return box
+
+ def create_asset(self, **params):
+ x, y, z = self.dimensions
+ parts = []
+
+
+ cabinet_bottom_height = self.cabinet_bottom_height
+ cabinet_top_height = self.cabinet_top_height
+
+ cabinet_bottom_factory = KitchenCabinetFactory(self.factory_seed, dimensions=(x, y-0.15, cabinet_bottom_height), drawer_only=True)
+ cabinet_bottom = cabinet_bottom_factory(i=0)
+ parts.append(cabinet_bottom)
+
+ surface.add_geomod(cabinet_bottom, geometry_nodes_add_cabinet_top, apply=True)
+
+ if not self.island:
+ # top
+ top_mid_width = uniform(1.0, 1.3)
+ cabinet_top_width = (y - top_mid_width) / 2.0 - 0.05
+
+ cabinet_top_factory = KitchenCabinetFactory(self.factory_seed, dimensions=(x / 2.0, cabinet_top_width, cabinet_top_height), drawer_only=False)
+ cabinet_top_left = cabinet_top_factory(i=0)
+ cabinet_top_right = cabinet_top_factory(i=1)
+
+ cabinet_top_left.location = (-x/4.0, 0.0, z-cabinet_top_height)
+ cabinet_top_right.location = (-x/4.0, y - cabinet_top_width, z-cabinet_top_height)
+
+ # hood / cab
+ # mid_style = choice(['range_hood', 'cabinet'])
+ # mid_style = 'range_hood'
+ mid_style = choice(['cabinet'])
+ if mid_style == 'range_hood':
+ range_hood_factory = RangeHoodFactory(self.factory_seed, dimensions=(x*0.66, top_mid_width + 0.15, cabinet_top_height))
+ top_mid = range_hood_factory(i=0)
+ top_mid.location = (-x*0.5, y/2.0, z-cabinet_top_height+0.05)
+
+ elif mid_style == 'cabinet':
+ cabinet_top_mid_factory = KitchenCabinetFactory(self.factory_seed, dimensions=(x*0.66, top_mid_width, cabinet_top_height * 0.8), drawer_only=False)
+ top_mid = cabinet_top_mid_factory(i=0)
+ top_mid.location = (-x/6.0, y/2.0 - top_mid_width / 2.0, z-(cabinet_top_height * 0.8))
+
+ else:
+ raise NotImplementedError
+
+ # parts += [sink, cabinet_top_left, cabinet_top_right, top_mid]
+ parts += [cabinet_top_left, cabinet_top_right, top_mid]
+
+ kitchen_space = butil.join_objects(parts)#[cabinet_bottom, sink, cabinet_top_left, cabinet_top_right, top_mid])
+
+ if not self.island:
+ kitchen_space.dimensions = self.dimensions
+ butil.apply_transform(kitchen_space)
+
+ tagging.tag_system.relabel_obj(kitchen_space)
+
+ return kitchen_space
+
+class KitchenIslandFactory(KitchenSpaceFactory):
+
+ def __init__(self, factory_seed):
+
+ super(KitchenIslandFactory, self).__init__(
+ factory_seed=factory_seed,
+ island=True,
+ )
\ No newline at end of file
diff --git a/infinigen/assets/shelves/large_shelf.py b/infinigen/assets/shelves/large_shelf.py
new file mode 100644
index 000000000..2ba1950ae
--- /dev/null
+++ b/infinigen/assets/shelves/large_shelf.py
@@ -0,0 +1,616 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_wood, shader_shelves_wood_sampler,
+ shader_shelves_white_metallic, shader_shelves_white_metallic_sampler,
+ shader_shelves_black_metallic, shader_shelves_black_metallic_sampler)
+
+
+@node_utils.to_nodegroup('nodegroup_screw_head', singleton=False, type='GeometryNodeTree')
+def nodegroup_screw_head(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Depth', 0.0050),
+ ('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'division_thickness', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'screw_width_gap', 0.5000),
+ ('NodeSocketFloat', 'screw_depth_gap', 0.0000)])
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Radius': group_input.outputs["Radius"],
+ 'Depth': group_input.outputs["Depth"]},
+ attrs={'fill_type': 'TRIANGLE_FAN'})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cylinder.outputs["Mesh"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["screw_width_gap"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["screw_width_gap"], 1: 0.0000})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["division_thickness"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': multiply_2, 'Z': multiply_3})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Translation': combine_xyz})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Y': subtract_1, 'Z': multiply_3})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Translation': combine_xyz_4})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_1, transform_6]})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_2, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_4, join_geometry_2]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_3})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_attachment', singleton=False, type='GeometryNodeTree')
+def nodegroup_attachment(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'attach_thickness', 0.0000),
+ ('NodeSocketFloat', 'attach_length', 0.0000),
+ ('NodeSocketFloat', 'attach_z_translation', 0.0000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'attach_gap', 0.5000),
+ ('NodeSocketFloat', 'attach_width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_width"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_length"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': add, 'Y': add_1, 'Z': group_input.outputs["attach_thickness"]})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["attach_gap"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: multiply_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': subtract_1, 'Y': add_2,
+ 'Z': group_input.outputs["attach_z_translation"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_1, transform]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_division_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_division_board(nw: NodeWrangler, material, tag_support=False):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'thickness', 0.0000),
+ ('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'depth', 0.0000),
+ ('NodeSocketFloat', 'z_translation', 0.0000),
+ ('NodeSocketFloat', 'x_translation', 0.0000),
+ ('NodeSocketFloat', 'screw_depth', 0.0000),
+ ('NodeSocketFloat', 'screw_radius', 0.0000),
+ ('NodeSocketFloat', 'screw_width_gap', 0.0000),
+ ('NodeSocketFloat', 'screw_depth_gap', 0.0000)])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': group_input.outputs["depth"],
+ 'Z': group_input.outputs["thickness"]})
+
+ if tag_support:
+ cube = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz})
+ else:
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ screw_head = nw.new_node(nodegroup_screw_head().name,
+ input_kwargs={'Depth': group_input.outputs["screw_depth"],
+ 'Radius': group_input.outputs["screw_radius"],
+ 'division_thickness': group_input.outputs["thickness"],
+ 'width': group_input.outputs["width"], 'depth': group_input.outputs["depth"],
+ 'screw_width_gap': group_input.outputs["screw_width_gap"],
+ 'screw_depth_gap': group_input.outputs["screw_depth_gap"]})
+
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [cube, screw_head]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["x_translation"],
+ 'Z': group_input.outputs["z_translation"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_bottom_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_bottom_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'thickness', 0.0000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'y_gap', 0.5000),
+ ('NodeSocketFloat', 'x_translation', 0.0000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'width', 0.0000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': group_input.outputs["thickness"],
+ 'Z': add})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: group_input.outputs["y_gap"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["x_translation"], 'Y': subtract,
+ 'Z': multiply_1})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_back_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_back_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': add, 'Z': add_1})
+
+ cube_2 = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz_4, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: -0.5000, 2: multiply},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_add, 'Z': multiply_1})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_2, 'Translation': combine_xyz_5})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_5},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_side_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_side_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'board_thickness', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'x_translation', 0.0000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["x_translation"], 'Z': multiply})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ side_board_thickness = nw.new_node(Nodes.Value, label='side_board_thickness')
+ side_board_thickness.outputs[0].default_value = kwargs['side_board_thickness']
+
+ shelf_depth = nw.new_node(Nodes.Value, label='shelf_depth')
+ shelf_depth.outputs[0].default_value = kwargs['shelf_depth']
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: shelf_depth, 1: 0.0040})
+
+ shelf_height = nw.new_node(Nodes.Value, label='shelf_height')
+ shelf_height.outputs[0].default_value = kwargs['shelf_height']
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: shelf_height, 1: 0.0020})
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: shelf_height, 1: -0.0010})
+ side_boards = []
+
+ for x in kwargs['side_board_x_translation']:
+ side_board_x_translation = nw.new_node(Nodes.Value, label='side_board_x_translation')
+ side_board_x_translation.outputs[0].default_value = x
+
+ side_board = nw.new_node(nodegroup_side_board().name,
+ input_kwargs={'board_thickness': side_board_thickness,
+ 'depth': add, 'height': add_1,
+ 'x_translation': side_board_x_translation})
+ side_boards.append(side_board)
+
+ shelf_width = nw.new_node(Nodes.Value, label='shelf_width')
+ shelf_width.outputs[0].default_value = kwargs['shelf_width']
+
+ backboard_thickness = nw.new_node(Nodes.Value, label='backboard_thickness')
+ backboard_thickness.outputs[0].default_value = kwargs['backboard_thickness']
+
+ add_side = nw.new_node(Nodes.Math, input_kwargs={0: shelf_width, 1: kwargs['side_board_thickness'] * 2})
+ back_board = nw.new_node(nodegroup_back_board().name,
+ input_kwargs={'width': add_side, 'thickness': backboard_thickness,
+ 'height': add_2, 'depth': shelf_depth})
+
+ bottom_board_y_gap = nw.new_node(Nodes.Value, label='bottom_board_y_gap')
+ bottom_board_y_gap.outputs[0].default_value = kwargs['bottom_board_y_gap']
+
+ bottom_board_height = nw.new_node(Nodes.Value, label='bottom_board_height')
+ bottom_board_height.outputs[0].default_value = kwargs['bottom_board_height']
+
+ bottom_boards = []
+ for i in range(len(kwargs['shelf_cell_width'])):
+
+ bottom_gap_x_translation = nw.new_node(Nodes.Value, label='bottom_gap_x_translation')
+ bottom_gap_x_translation.outputs[0].default_value = kwargs['bottom_gap_x_translation'][i]
+
+ shelf_cell_width = nw.new_node(Nodes.Value, label='shelf_cell_width')
+ shelf_cell_width.outputs[0].default_value = kwargs['shelf_cell_width'][i]
+
+ bottomboard = nw.new_node(nodegroup_bottom_board().name,
+ input_kwargs={'thickness': side_board_thickness, 'depth': shelf_depth,
+ 'y_gap': bottom_board_y_gap, 'x_translation': bottom_gap_x_translation,
+ 'height': bottom_board_height, 'width': shelf_cell_width})
+
+ bottom_boards.append(bottomboard)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [back_board] + side_boards + bottom_boards})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': realize_instances,
+ 'Material': surface.shaderfunc_to_material(kwargs['frame_material'])})
+
+ division_board_thickness = nw.new_node(Nodes.Value, label='division_board_thickness')
+ division_board_thickness.outputs[0].default_value = kwargs['division_board_thickness']
+
+ division_boards = []
+ for i in range(len(kwargs['shelf_cell_width'])):
+ for j in range(len(kwargs['division_board_z_translation'])):
+
+ division_board_z_translation = nw.new_node(Nodes.Value, label='division_board_z_translation')
+ division_board_z_translation.outputs[0].default_value = kwargs['division_board_z_translation'][j]
+
+ division_board_x_translation = nw.new_node(Nodes.Value, label='division_board_x_translation')
+ division_board_x_translation.outputs[0].default_value = kwargs['division_board_x_translation'][i]
+
+ shelf_cell_width = nw.new_node(Nodes.Value, label='shelf_cell_width')
+ shelf_cell_width.outputs[0].default_value = kwargs['shelf_cell_width'][i]
+
+ screw_depth_head = nw.new_node(Nodes.Value, label='screw_depth_head')
+ screw_depth_head.outputs[0].default_value = kwargs['screw_depth_head']
+
+ screw_head_radius = nw.new_node(Nodes.Value, label='screw_head_radius')
+ screw_head_radius.outputs[0].default_value = kwargs['screw_head_radius']
+
+ screw_width_gap = nw.new_node(Nodes.Value, label='screw_width_gap')
+ screw_width_gap.outputs[0].default_value = kwargs['screw_width_gap']
+
+ screw_depth_gap = nw.new_node(Nodes.Value, label='screw_depth_gap')
+ screw_depth_gap.outputs[0].default_value = kwargs['screw_depth_gap']
+
+ division_board = nw.new_node(nodegroup_division_board(material=kwargs['board_material'],
+ tag_support=kwargs.get('tag_support', False)).name,
+ input_kwargs={'thickness': division_board_thickness,
+ 'width': shelf_cell_width,
+ 'depth': shelf_depth,
+ 'z_translation': division_board_z_translation,
+ 'x_translation': division_board_x_translation,
+ 'screw_depth': screw_depth_head,
+ 'screw_radius': screw_head_radius,
+ 'screw_width_gap': screw_width_gap,
+ 'screw_depth_gap': screw_depth_gap})
+ division_boards.append(division_board)
+
+ attach_thickness = nw.new_node(Nodes.Value, label='attach_thickness')
+ attach_thickness.outputs[0].default_value = kwargs['attach_thickness']
+
+ attach_length = nw.new_node(Nodes.Value, label='attach_length')
+ attach_length.outputs[0].default_value = kwargs['attach_length']
+
+ attach_z_translation = nw.new_node(Nodes.Value, label='attach_z_translation')
+ attach_z_translation.outputs[0].default_value = kwargs['attach_z_translation']
+
+ attach_gap = nw.new_node(Nodes.Value, label='attach_gap')
+ attach_gap.outputs[0].default_value = kwargs['attach_gap']
+
+ attach_width = nw.new_node(Nodes.Value, label='attach_width')
+ attach_width.outputs[0].default_value = kwargs['attach_width']
+
+ join_geometry_k = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': division_boards})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': join_geometry_k,
+ 'Material': surface.shaderfunc_to_material(kwargs['board_material'])})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ realize_instances_3 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_3})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances_3})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+class LargeShelfBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(LargeShelfBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {}
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('shelf_depth', None) is None:
+ params['shelf_depth'] = np.clip(normal(0.26, 0.03), 0.18, 0.36)
+ if params.get('side_board_thickness', None) is None:
+ params['side_board_thickness'] = np.clip(normal(0.02, 0.002), 0.015, 0.025)
+ if params.get('back_board_thickness', None) is None:
+ params['backboard_thickness'] = 0.01
+ if params.get('bottom_board_y_gap', None) is None:
+ params['bottom_board_y_gap'] = uniform(0.01, 0.05)
+ if params.get('bottom_board_height', None) is None:
+ params['bottom_board_height'] = (np.clip(normal(0.083, 0.01), 0.05, 0.11) *
+ np.random.choice([1., 0.], p=[0.8, 0.2]))
+ if params.get('division_board_thickness', None) is None:
+ params['division_board_thickness'] = np.clip(normal(0.02, 0.002), 0.015, 0.025)
+ if params.get('screw_depth_head', None) is None:
+ params['screw_depth_head'] = uniform(0.001, 0.004)
+ if params.get('screw_head_radius', None) is None:
+ params['screw_head_radius'] = uniform(0.001, 0.004)
+ if params.get('screw_width_gap', None) is None:
+ params['screw_width_gap'] = uniform(0.0, 0.02)
+ if params.get('screw_depth_gap', None) is None:
+ params['screw_depth_gap'] = uniform(0.025, 0.06)
+ if params.get('attach_length', None) is None:
+ params['attach_length'] = uniform(0.05, 0.1)
+ if params.get('attach_width', None) is None:
+ params['attach_width'] = uniform(0.01, 0.025)
+ if params.get('attach_thickness', None) is None:
+ params['attach_thickness'] = uniform(0.002, 0.005)
+ if params.get('attach_gap', None) is None:
+ params['attach_gap'] = uniform(0.0, 0.05)
+ if params.get('shelf_cell_width', None) is None:
+ num_h_cells = randint(1, 4)
+ shelf_cell_width = []
+ for i in range(num_h_cells):
+ shelf_cell_width.append(np.random.choice([0.76, 0.36], p=[0.5, 0.5]) *
+ np.clip(normal(1., 0.1), 0.75, 1.25))
+ params['shelf_cell_width'] = shelf_cell_width
+ if params.get('shelf_cell_height', None) is None:
+ num_v_cells = randint(3, 8)
+ shelf_cell_height = []
+ for i in range(num_v_cells):
+ shelf_cell_height.append(0.3 * np.clip(normal(1., 0.1), 0.75, 1.25))
+ params['shelf_cell_height'] = shelf_cell_height
+
+ params = self.update_translation_params(params)
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.4, 0.3, 0.3])
+ if params.get('board_material', None) is None:
+ params['board_material'] = params['frame_material']
+
+ params = self.get_material_func(params)
+ params['tag_support'] = True
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ white_wood_params = shader_shelves_white_sampler()
+ black_wood_params = shader_shelves_black_wood_sampler()
+ normal_wood_params = shader_shelves_wood_sampler()
+ if params['frame_material'] == 'white':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['frame_material'] = shader_shelves_white
+ elif params['frame_material'] == 'black_wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_black_wood(x, z_axis_texture=True)
+ elif params['frame_material'] == 'wood':
+ if randomness:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params, z_axis_texture=True)
+ else:
+ params['frame_material'] = lambda x: shader_shelves_wood(x, z_axis_texture=True)
+
+ if params['board_material'] == 'white':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_white(x, **white_wood_params)
+ else:
+ params['board_material'] = shader_shelves_white
+ elif params['board_material'] == 'black_wood':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_black_wood(x, **black_wood_params)
+ else:
+ params['board_material'] = shader_shelves_black_wood
+ elif params['board_material'] == 'wood':
+ if randomness:
+ params['board_material'] = lambda x: shader_shelves_wood(x, **normal_wood_params)
+ else:
+ params['board_material'] = shader_shelves_wood
+
+ return params
+
+ def update_translation_params(self, params):
+ cell_widths = params['shelf_cell_width']
+ cell_heights = params['shelf_cell_height']
+ side_thickness = params['side_board_thickness']
+ div_thickness = params['division_board_thickness']
+
+ # get shelf_width and shelf_height
+ width = (len(cell_widths) - 1) * side_thickness * 2 + (len(cell_widths) - 1) * 0.001
+ height = (len(cell_heights) + 1) * div_thickness + params['bottom_board_height']
+ for w in cell_widths:
+ width += w
+ for h in cell_heights:
+ height += h
+
+ params['shelf_width'] = width
+ params['shelf_height'] = height
+ params['attach_z_translation'] = height - div_thickness
+
+ # get side_board_x_translation
+ dist = - (width + side_thickness) / 2.
+ side_board_x_translation = [dist]
+
+ for w in cell_widths:
+ dist += side_thickness + w
+ side_board_x_translation.append(dist)
+ dist += side_thickness + 0.001
+ side_board_x_translation.append(dist)
+ side_board_x_translation = side_board_x_translation[:-1]
+
+ # get division_board_z_translation
+ dist = params['bottom_board_height'] + div_thickness / 2.
+ division_board_z_translation = [dist]
+ for h in cell_heights:
+ dist += h + div_thickness
+ division_board_z_translation.append(dist)
+
+ # get division_board_x_translation
+ division_board_x_translation = []
+ for i in range(len(cell_widths)):
+ division_board_x_translation.append((side_board_x_translation[2 * i] + side_board_x_translation[2 * i+1]) / 2.)
+
+ params['side_board_x_translation'] = side_board_x_translation
+ params['division_board_x_translation'] = division_board_x_translation
+ params['division_board_z_translation'] = division_board_z_translation
+ params['bottom_gap_x_translation'] = division_board_x_translation
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+
+ if params.get('ret_params', False):
+ return obj, obj_params
+
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class LargeShelfFactory(LargeShelfBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (
+ uniform(0.25, 0.35),
+ uniform(0.3, 2.0),
+ uniform(0.9, 2.0)
+ )
+
+ params['bottom_board_height'] = 0.083
+ params['shelf_depth'] = params['Dimensions'][0] - 0.01
+ num_h = int((params['Dimensions'][2] - 0.083) / 0.3)
+ params['shelf_cell_height'] = [(params['Dimensions'][2] - 0.083) / num_h for _ in range(num_h)]
+ num_v = max(int(params['Dimensions'][1] / 0.5), 1)
+ params['shelf_cell_width'] = [params['Dimensions'][1] / num_v for _ in range(num_v)]
+ return params
diff --git a/infinigen/assets/shelves/simple_bookcase.py b/infinigen/assets/shelves/simple_bookcase.py
new file mode 100644
index 000000000..6ea9796a9
--- /dev/null
+++ b/infinigen/assets/shelves/simple_bookcase.py
@@ -0,0 +1,518 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials.shelf_shaders import get_shelf_material
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+
+
+@node_utils.to_nodegroup('nodegroup_attach_gadget', singleton=False, type='GeometryNodeTree')
+def nodegroup_attach_gadget(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'division_thickness', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000), ('NodeSocketFloat', 'attach_thickness', 0.5000),
+ ('NodeSocketFloat', 'attach_width', 0.5000), ('NodeSocketFloat', 'attach_back_len', 0.5000),
+ ('NodeSocketFloat', 'attach_top_len', 0.5000), ('NodeSocketFloat', 'depth', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_width"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_top_len"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_thickness"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: add_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["height"],
+ 1: group_input.outputs["division_thickness"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply, 'Z': subtract_1})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["attach_back_len"], 1: 0.0000})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_2, 'Z': add_4})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={
+ 'Size': combine_xyz_1,
+ 'Vertices X': 5,
+ 'Vertices Y': 5,
+ 'Vertices Z': 5
+ })
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_4}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: multiply_2},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1, 'Z': subtract_2})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_1, 'Translation': combine_xyz_3})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'attach1': transform, 'attach2': transform_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_screw_head', singleton=False, type='GeometryNodeTree')
+def nodegroup_screw_head(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Depth', 0.0050),
+ ('NodeSocketFloatDistance', 'Radius', 1.0000), ('NodeSocketFloat', 'bottom_gap', 0.5000),
+ ('NodeSocketFloat', 'division_thickness', 0.5000), ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000), ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'screw_gap', 0.5000)])
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={
+ 'Radius': group_input.outputs["Radius"],
+ 'Depth': group_input.outputs["Depth"]
+ }, attrs={'fill_type': 'TRIANGLE_FAN'})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': cylinder.outputs["Mesh"],
+ 'Rotation': (0.0000, 1.5708, 0.0000)
+ })
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["screw_gap"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["division_thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: multiply_2},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': subtract, 'Z': subtract_1})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_1})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: group_input.outputs["bottom_gap"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': subtract, 'Z': add_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Translation': combine_xyz})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply, 'Y': multiply_3, 'Z': subtract_1})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_2})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: add_1})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Z': multiply_4})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_3})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': multiply_3, 'Z': add_1})
+
+ transform_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_4})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [transform_2, transform_1, transform_3, transform_5, transform_6]
+ })
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_2, 'Scale': (-1.0000, 1.0000, 1.0000)})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_4, join_geometry_2]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_3},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_back_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_back_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'thickness', 0.5000), ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': add, 'Z': add_1})
+
+ cube_2 = nw.new_node(Nodes.MeshCube, input_kwargs={
+ 'Size': combine_xyz_4,
+ 'Vertices X': 10,
+ 'Vertices Y': 10,
+ 'Vertices Z': 10
+ })
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: -0.5000, 2: multiply},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_add, 'Z': multiply_1})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube_2, 'Translation': combine_xyz_5})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_5},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_division_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_division_board(nw: NodeWrangler, tag_support=False):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'board_thickness', 0.0000),
+ ('NodeSocketFloat', 'depth', 0.5000), ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'side_thickness', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["side_thickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': subtract,
+ 'Y': add,
+ 'Z': group_input.outputs["board_thickness"]
+ })
+
+ if tag_support:
+ cube_1 = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz_3})
+ else:
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={
+ 'Size': combine_xyz_3,
+ 'Vertices X': 10,
+ 'Vertices Y': 10,
+ 'Vertices Z': 10
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': cube_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_division_boards', singleton=False, type='GeometryNodeTree')
+def nodegroup_division_boards(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000), ('NodeSocketFloat', 'gap', 0.5000),
+ ('NodeSocketGeometry', 'Geometry', None)])
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances,
+ input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["gap"], 1: multiply})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': realize_instances_1, 'Translation': combine_xyz_1})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: add})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': realize_instances_1, 'Translation': combine_xyz_2})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': subtract})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': realize_instances_1, 'Translation': combine_xyz})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={
+ 'board1': transform_2,
+ 'board2': transform_3,
+ 'board3': transform_4
+ }, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_side_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_side_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'board_thickness', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000), ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'width', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_thickness"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["depth"], 1: 0.0000})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["height"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1, 'Z': add_2})
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_2, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Z': multiply_1})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: 0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Z': multiply_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ side_board_thickness = nw.new_node(Nodes.Value, label='side_board_thickness')
+ side_board_thickness.outputs[0].default_value = kwargs['side_board_thickness']
+
+ shelf_depth = nw.new_node(Nodes.Value, label='shelf_depth')
+ shelf_depth.outputs[0].default_value = kwargs['depth']
+
+ shelf_height = nw.new_node(Nodes.Value, label='shelf_height')
+ shelf_height.outputs[0].default_value = kwargs['height']
+
+ shelf_width = nw.new_node(Nodes.Value, label='shelf_width')
+ shelf_width.outputs[0].default_value = kwargs['width']
+
+ side_board = nw.new_node(nodegroup_side_board().name, input_kwargs={
+ 'board_thickness': side_board_thickness,
+ 'depth': shelf_depth,
+ 'height': shelf_height,
+ 'width': shelf_width
+ })
+
+ division_board_thickness = nw.new_node(Nodes.Value, label='division_board_thickness')
+ division_board_thickness.outputs[0].default_value = kwargs['division_board_thickness']
+
+ bottom_gap = nw.new_node(Nodes.Value, label='bottom_gap')
+ bottom_gap.outputs[0].default_value = kwargs['bottom_gap']
+
+ division_board = nw.new_node(nodegroup_division_board(tag_support=kwargs['tag_support']).name,
+ input_kwargs={
+ 'board_thickness': division_board_thickness,
+ 'depth': shelf_depth,
+ 'width': shelf_width,
+ 'side_thickness': side_board_thickness
+ })
+
+ division_boards = nw.new_node(nodegroup_division_boards().name, input_kwargs={
+ 'thickness': division_board_thickness,
+ 'height': shelf_height,
+ 'gap': bottom_gap,
+ 'Geometry': division_board
+ })
+
+ backboard_thickness = nw.new_node(Nodes.Value, label='backboard_thickness')
+ backboard_thickness.outputs[0].default_value = kwargs['backboard_thickness']
+
+ back_board = nw.new_node(nodegroup_back_board().name, input_kwargs={
+ 'width': shelf_width,
+ 'thickness': backboard_thickness,
+ 'height': shelf_height,
+ 'depth': shelf_depth
+ })
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [side_board, division_boards.outputs["board1"], division_boards.outputs["board2"],
+ back_board, division_boards.outputs["board3"]]
+ })
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': realize_instances,
+ 'Material': kwargs['frame_material']
+ })
+
+ screw_depth_head = nw.new_node(Nodes.Value, label='screw_depth_head')
+ screw_depth_head.outputs[0].default_value = kwargs['screw_head_depth']
+
+ screw_head_radius = nw.new_node(Nodes.Value, label='screw_head_radius')
+ screw_head_radius.outputs[0].default_value = kwargs['screw_head_radius']
+
+ screw_head_gap = nw.new_node(Nodes.Value, label='screw_head_gap')
+ screw_head_gap.outputs[0].default_value = kwargs['screw_head_dist']
+
+ screw_head = nw.new_node(nodegroup_screw_head().name, input_kwargs={
+ 'Depth': screw_depth_head,
+ 'Radius': screw_head_radius,
+ 'bottom_gap': bottom_gap,
+ 'division_thickness': division_board_thickness,
+ 'width': shelf_width,
+ 'height': shelf_height,
+ 'depth': shelf_depth,
+ 'screw_gap': screw_head_gap
+ })
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': screw_head})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': realize_instances_1,
+ 'Material': kwargs['metal_material']
+ })
+
+ attach_thickness = nw.new_node(Nodes.Value, label='attach_thickness')
+ attach_thickness.outputs[0].default_value = kwargs['attach_thickness']
+
+ attach_width = nw.new_node(Nodes.Value, label='attach_width')
+ attach_width.outputs[0].default_value = kwargs['attach_width']
+
+ attach_back_length = nw.new_node(Nodes.Value, label='attach_back_length')
+ attach_back_length.outputs[0].default_value = kwargs['attach_back_length']
+
+ attach_top_length = nw.new_node(Nodes.Value, label='attach_top_length')
+ attach_top_length.outputs[0].default_value = kwargs['attach_top_length']
+
+ attach_gadget = nw.new_node(nodegroup_attach_gadget().name, input_kwargs={
+ 'division_thickness': division_board_thickness,
+ 'height': shelf_height,
+ 'attach_thickness': attach_thickness,
+ 'attach_width': attach_width,
+ 'attach_back_len': attach_back_length,
+ 'attach_top_len': attach_top_length,
+ 'depth': shelf_depth
+ })
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [attach_gadget.outputs["attach1"], attach_gadget.outputs["attach2"]]
+ })
+
+ realize_instances_2 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_2})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': realize_instances_2,
+ 'Material': kwargs['metal_material']
+ })
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [set_material, set_material_1, set_material_2]})
+
+ realize_instances_3 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances_3})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+class SimpleBookcaseBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(SimpleBookcaseBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('depth', None) is None:
+ params['depth'] = np.clip(normal(0.3, 0.05), 0.15, 0.45)
+ if params.get('width', None) is None:
+ params['width'] = np.clip(normal(0.5, 0.1), 0.25, 0.75)
+ if params.get('height', None) is None:
+ params['height'] = np.clip(normal(0.8, 0.1), 0.5, 1.0)
+ params['side_board_thickness'] = uniform(0.005, 0.03)
+ params['division_board_thickness'] = np.clip(normal(0.015, 0.005), 0.005, 0.025)
+ params['bottom_gap'] = np.clip(normal(0.14, 0.05), 0.0, 0.2)
+ params['backboard_thickness'] = uniform(0.01, 0.02)
+ params['screw_head_depth'] = uniform(0.002, 0.008)
+ params['screw_head_radius'] = uniform(0.003, 0.008)
+ params['screw_head_dist'] = uniform(0.03, 0.1)
+ params['attach_thickness'] = uniform(0.002, 0.005)
+ params['attach_width'] = uniform(0.01, 0.04)
+ params['attach_top_length'] = uniform(0.03, 0.1)
+ params['attach_back_length'] = uniform(0.02, 0.05)
+ params['frame_material'] = get_shelf_material('white')
+ params['metal_material'] = get_shelf_material('metal')
+ params['tag_support'] = True
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, apply=True, attributes=[], input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class SimpleBookcaseFactory(SimpleBookcaseBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (uniform(0.25, 0.4), uniform(0.5, 0.7), uniform(0.7, 0.9))
+ params['depth'] = params['Dimensions'][0] - 0.015
+ params['width'] = params['Dimensions'][1]
+ params['height'] = params['Dimensions'][2]
+ return params
diff --git a/infinigen/assets/shelves/simple_desk.py b/infinigen/assets/shelves/simple_desk.py
new file mode 100644
index 000000000..440ae9fe5
--- /dev/null
+++ b/infinigen/assets/shelves/simple_desk.py
@@ -0,0 +1,267 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+from infinigen.assets.materials.shelf_shaders import (
+ shader_shelves_white, shader_shelves_white_sampler,
+ shader_shelves_black_wood, shader_shelves_black_wood_sampler,
+ shader_shelves_white_metallic, shader_shelves_white_metallic_sampler,
+ shader_shelves_black_metallic, shader_shelves_black_metallic_sampler)
+
+
+@node_utils.to_nodegroup('nodegroup_table_legs', singleton=False, type='GeometryNodeTree')
+def nodegroup_table_legs(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'thickness', 0.5000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloatDistance', 'radius', 0.0200),
+ ('NodeSocketFloat', 'width', 0.5000),
+ ('NodeSocketFloat', 'depth', 0.5000),
+ ('NodeSocketFloat', 'dist', 0.5000)])
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["height"], 1: group_input.outputs["thickness"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Radius': group_input.outputs["radius"], 'Depth': subtract, 'Vertices': 128})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["width"]}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["dist"], 1: 0.0000})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={1: group_input.outputs["depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': subtract_2, 'Z': multiply_2})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_2})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': subtract_2, 'Z': multiply_2})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_3})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': multiply_4, 'Z': multiply_2})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_4})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': multiply_4, 'Z': multiply_2})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_5})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform, transform_2, transform_3, transform_4]})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_table_top', singleton=False, type='GeometryNodeTree')
+def nodegroup_table_top(nw: NodeWrangler, tag_support=True):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'depth', 0.0000),
+ ('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'height', 0.5000),
+ ('NodeSocketFloat', 'thickness', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["width"], 'Y': group_input.outputs["depth"],
+ 'Z': add})
+
+ if tag_support:
+ cube = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz})
+
+ else:
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 10, 'Vertices Y': 10, 'Vertices Z': 10})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["height"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': subtract})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ table_depth = nw.new_node(Nodes.Value, label='table_depth')
+ table_depth.outputs[0].default_value = kwargs['depth']
+
+ table_width = nw.new_node(Nodes.Value, label='table_width')
+ table_width.outputs[0].default_value = kwargs['width']
+
+ table_height = nw.new_node(Nodes.Value, label='table_height')
+ table_height.outputs[0].default_value = kwargs['height']
+
+ top_thickness = nw.new_node(Nodes.Value, label='top_thickness')
+ top_thickness.outputs[0].default_value = kwargs['thickness']
+
+ table_top = nw.new_node(nodegroup_table_top(tag_support=True).name,
+ input_kwargs={'depth': table_depth, 'width': table_width, 'height': table_height,
+ 'thickness': top_thickness})
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': table_top,
+ 'Material': surface.shaderfunc_to_material(kwargs['top_material'])})
+
+ leg_radius = nw.new_node(Nodes.Value, label='leg_radius')
+ leg_radius.outputs[0].default_value = kwargs['leg_radius']
+
+ leg_center_to_edge = nw.new_node(Nodes.Value, label='leg_center_to_edge')
+ leg_center_to_edge.outputs[0].default_value = kwargs['leg_dist']
+
+ table_legs = nw.new_node(nodegroup_table_legs().name,
+ input_kwargs={'thickness': top_thickness, 'height': table_height, 'radius': leg_radius,
+ 'width': table_width, 'depth': table_depth, 'dist': leg_center_to_edge})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': table_legs,
+ 'Material': surface.shaderfunc_to_material(kwargs['leg_material'])})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, 1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform},
+ attrs={'is_active_output': True})
+
+
+class SimpleDeskBaseFactory(AssetFactory):
+
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(SimpleDeskBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = params
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('depth', None) is None:
+ params['depth'] = np.clip(normal(0.6, 0.05), 0.45, 0.7)
+ if params.get('width', None) is None:
+ params['width'] = np.clip(normal(1.0, 0.1), 0.7, 1.3)
+ if params.get('height', None) is None:
+ params['height'] = np.clip(normal(0.73, 0.05), 0.6, 0.83)
+ if params.get('top_material', None) is None:
+ params['top_material'] = np.random.choice(['white', 'black_wood'])
+ if params.get('leg_material', None) is None:
+ params['leg_material'] = np.random.choice(['white', 'black'])
+ if params.get('leg_radius', None) is None:
+ params['leg_radius'] = uniform(0.01, 0.025)
+ if params.get('leg_dist', None) is None:
+ params['leg_dist'] = uniform(0.035, 0.07)
+ if params.get('thickness', None) is None:
+ params['thickness'] = uniform(0.01, 0.03)
+
+ params = self.get_material_func(params)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ if params['top_material'] == 'white':
+ if randomness:
+ params['top_material'] = lambda x: shader_shelves_white(x, **shader_shelves_white_sampler())
+ else:
+ params['top_material'] = shader_shelves_white
+ elif params['top_material'] == 'black_wood':
+ if randomness:
+ params['top_material'] = lambda x: shader_shelves_black_wood(x, **shader_shelves_black_wood_sampler())
+ else:
+ params['top_material'] = shader_shelves_black_wood
+ else:
+ raise NotImplementedError
+
+ if params['leg_material'] == 'white':
+ if randomness:
+ params['leg_material'] = lambda x: shader_shelves_white_metallic(x, **shader_shelves_white_metallic_sampler())
+ else:
+ params['leg_material'] = shader_shelves_white_metallic
+ elif params['leg_material'] == 'black':
+ if randomness:
+ params['leg_material'] = lambda x: shader_shelves_black_metallic(x, **shader_shelves_black_metallic_sampler())
+ else:
+ params['leg_material'] = shader_shelves_black_metallic
+ else:
+ raise NotImplementedError
+
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, attributes=[], apply=True, input_kwargs=obj_params)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class SimpleDeskFactory(SimpleDeskBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (uniform(0.5, 0.75),
+ uniform(0.8, 2),
+ uniform(0.6, 0.8))
+ params['depth'] = params['Dimensions'][0]
+ params['width'] = params['Dimensions'][1]
+ params['height'] = params['Dimensions'][2]
+ return params
+
+class SidetableDeskFactory(SimpleDeskBaseFactory):
+ def sample_params(self):
+ params = dict()
+ w = 0.55 * normal(1, 0.1)
+ params['Dimensions'] = (w, w, w * normal(1, 0.05))
+ params['depth'] = params['Dimensions'][0]
+ params['width'] = params['Dimensions'][1]
+ params['height'] = params['Dimensions'][2]
+ return params
+
diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py
new file mode 100644
index 000000000..14dc77257
--- /dev/null
+++ b/infinigen/assets/shelves/single_cabinet.py
@@ -0,0 +1,235 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube, blender_rotate
+from infinigen.assets.shelves.large_shelf import LargeShelfBaseFactory
+from infinigen.assets.shelves.doors import CabinetDoorBaseFactory
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.assets.utils.object import new_bbox
+
+def geometry_cabinet_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ right_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['door'][0]})
+ left_door_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['door'][1]})
+ shelf_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': kwargs['shelf']})
+
+ doors = []
+ transform_r = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': right_door_info.outputs['Geometry'],
+ 'Translation': kwargs['door_hinge_pos'][0],
+ 'Rotation': (0, 0, kwargs['door_open_angle'])})
+ doors.append(transform_r)
+ if len(kwargs['door_hinge_pos']) > 1:
+ transform_l = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': left_door_info.outputs['Geometry'],
+ 'Translation': kwargs['door_hinge_pos'][1],
+ 'Rotation': (0, 0, kwargs['door_open_angle'])})
+ doors.append(transform_l)
+
+ attaches = []
+ for pos in kwargs['attach_pos']:
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0006, 0.0200, 0.04500)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': -0.0100})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz})
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': (0.0005, 0.0340, 0.0200)})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, cube_1]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Translation': (0.0000, -0.0170, 0.0000)})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_1, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_2, 'Translation': pos})
+
+ attaches.append(transform_3)
+
+ join_geometry_a = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': attaches})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': doors + [join_geometry_a]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+class SingleCabinetBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(SingleCabinetBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.shelf_params = {}
+ self.door_params = {}
+ self.mat_params = {}
+ self.shelf_fac = LargeShelfBaseFactory(factory_seed)
+ self.door_fac = CabinetDoorBaseFactory(factory_seed)
+ with FixedSeed(factory_seed):
+ self.params = self.sample_params()
+
+ def sample_params(self):
+ # Update fac params
+ pass
+
+ def get_material_params(self):
+ with FixedSeed(self.factory_seed):
+ params = self.mat_params.copy()
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = np.random.choice(['white', 'black_wood', 'wood'], p=[0.5, 0.2, 0.3])
+ return params
+
+ def get_shelf_params(self, i=0):
+ params = self.shelf_params.copy()
+ if params.get('shelf_cell_width', None) is None:
+ params['shelf_cell_width'] = [np.random.choice([0.76, 0.36], p=[0.5, 0.5]) *
+ np.clip(normal(1., 0.1), 0.75, 1.25)]
+ if params.get('shelf_cell_height', None) is None:
+ num_v_cells = randint(3, 7)
+ shelf_cell_height = []
+ for i in range(num_v_cells):
+ shelf_cell_height.append(0.3 * np.clip(normal(1., 0.06), 0.75, 1.25))
+ params['shelf_cell_height'] = shelf_cell_height
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = self.mat_params['frame_material']
+
+ return params
+
+ def get_door_params(self, i=0):
+ params = self.door_params.copy()
+
+ # get door params
+ shelf_width = self.shelf_params['shelf_width'] + self.shelf_params['side_board_thickness'] * 2
+ if params.get('door_width', None) is None:
+ if shelf_width < 0.55:
+ params['door_width'] = shelf_width
+ params['num_door'] = 1
+ else:
+ params['door_width'] = shelf_width / 2. - 0.0005
+ params['num_door'] = 2
+ if params.get('door_height', None) is None:
+ params['door_height'] = (self.shelf_params['division_board_z_translation'][-1] -
+ self.shelf_params['division_board_z_translation'][0] +
+ self.shelf_params['division_board_thickness'])
+ if len(self.shelf_params['division_board_z_translation']) > 5 and \
+ np.random.choice([True, False], p=[0.5, 0.5]):
+ params['door_height'] = (self.shelf_params['division_board_z_translation'][3] -
+ self.shelf_params['division_board_z_translation'][0] +
+ self.shelf_params['division_board_thickness'])
+ if params.get('frame_material', None) is None:
+ params['frame_material'] = self.mat_params['frame_material']
+
+ return params
+
+ def get_cabinet_params(self, i=0):
+ params = dict()
+
+ shelf_width = self.shelf_params['shelf_width'] + self.shelf_params['side_board_thickness'] * 2
+ if self.door_params['num_door'] == 1:
+ params['door_hinge_pos'] = [(self.shelf_params['shelf_depth'] / 2. + 0.0025, -shelf_width / 2.,
+ self.shelf_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+ params['attach_pos'] = [
+ (self.shelf_params['shelf_depth'] / 2., -self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']
+ ]
+ elif self.door_params['num_door'] == 2:
+ params['door_hinge_pos'] = [(self.shelf_params['shelf_depth'] / 2. + 0.008, -shelf_width / 2.,
+ self.shelf_params['bottom_board_height']),
+ (self.shelf_params['shelf_depth'] / 2. + 0.008, shelf_width / 2.,
+ self.shelf_params['bottom_board_height'])]
+ params['door_open_angle'] = 0
+ params['attach_pos'] = [
+ (self.shelf_params['shelf_depth'] / 2., -self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']
+ ] + [
+ (self.shelf_params['shelf_depth'] / 2., self.shelf_params['shelf_width'] / 2.,
+ self.shelf_params['bottom_board_height'] + z) for z in self.door_params['attach_height']
+ ]
+ else:
+ raise NotImplementedError
+
+ return params
+
+ def get_cabinet_components(self, i):
+ # update material params
+ self.mat_params = self.get_material_params()
+
+ # create shelf
+ shelf_params = self.get_shelf_params(i=i)
+ self.shelf_fac.params = shelf_params
+ shelf, shelf_params = self.shelf_fac.create_asset(i=i, ret_params=True)
+ shelf.name = 'cabinet_frame'
+ self.shelf_params = shelf_params
+
+ # create doors
+ door_params = self.get_door_params(i=i)
+ self.door_fac.params = door_params
+ self.door_fac.params['door_left_hinge'] = False
+ right_door, door_obj_params = self.door_fac.create_asset(i=i, ret_params=True)
+ right_door.name = 'cabinet_right_door'
+ self.door_fac.params = door_obj_params
+ self.door_fac.params['door_left_hinge'] = True
+ left_door, _ = self.door_fac.create_asset(i=i, ret_params=True)
+ left_door.name = 'cabinet_left_door'
+ self.door_params = door_obj_params
+
+ return shelf, right_door, left_door
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ shelf, right_door, left_door = self.get_cabinet_components(i=i)
+
+ # create cabinet
+ cabinet_params = self.get_cabinet_params(i=i)
+ surface.add_geomod(obj, geometry_cabinet_nodes, attributes=[], apply=True, input_kwargs={
+ 'door': [right_door, left_door],
+ 'shelf': shelf,
+ 'door_hinge_pos': cabinet_params['door_hinge_pos'],
+ 'door_open_angle': cabinet_params['door_open_angle'],
+ 'attach_pos': cabinet_params['attach_pos']
+ })
+ butil.delete([left_door, right_door])
+ obj = butil.join_objects([shelf, obj])
+
+ tagging.tag_system.relabel_obj(obj)
+ return obj
+
+
+class SingleCabinetFactory(SingleCabinetBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (
+ uniform(0.25, 0.35),
+ uniform(0.3, 0.7),
+ uniform(0.9, 1.8)
+ )
+
+ params['bottom_board_height'] = 0.083
+ params['shelf_depth'] = params['Dimensions'][0] - 0.01
+ num_h = int((params['Dimensions'][2] - 0.083) / 0.3)
+ params['shelf_cell_height'] = [(params['Dimensions'][2] - 0.083) / num_h for _ in range(num_h)]
+ params['shelf_cell_width'] = [params['Dimensions'][1]]
+ self.shelf_params = params
+ self.dims = params['Dimensions']
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ x,y,z = self.dims
+ return new_bbox(-x/2 * 1.2, x/2 * 1.2, -y/2 * 1.2, y/2 * 1.2, 0, (z + 0.083) * 1.02)
diff --git a/infinigen/assets/shelves/triangle_shelf.py b/infinigen/assets/shelves/triangle_shelf.py
new file mode 100644
index 000000000..3eb60ef98
--- /dev/null
+++ b/infinigen/assets/shelves/triangle_shelf.py
@@ -0,0 +1,870 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+from numpy.random import uniform, normal, randint
+
+from infinigen.assets.materials.shelf_shaders import get_shelf_material
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+import bpy
+
+from infinigen.assets.shelves.utils import nodegroup_tagged_cube
+
+
+@node_utils.to_nodegroup('nodegroup_table_profile', singleton=False, type='GeometryNodeTree')
+def nodegroup_table_profile(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketInt', 'Profile N-gon', 4),
+ ('NodeSocketFloat', 'Profile Width', 1.0000), ('NodeSocketFloat', 'Profile Aspect Ratio', 1.0000),
+ ('NodeSocketFloat', 'Profile Fillet Ratio', 0.2000)])
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 0.7071
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={
+ 'Resolution': group_input.outputs["Profile N-gon"],
+ 'Radius': value
+ })
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: 3.1416, 1: group_input.outputs["Profile N-gon"]},
+ attrs={'operation': 'DIVIDE'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': divide})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Rotation': combine_xyz_1})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Profile Aspect Ratio"],
+ 1: group_input.outputs["Profile Width"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Profile Width"],
+ 'Y': multiply,
+ 'Z': 1.0000
+ })
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_2, 'Scale': combine_xyz})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Profile Width"],
+ 1: group_input.outputs["Profile Fillet Ratio"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ fillet_curve_1 = nw.new_node('GeometryNodeFilletCurve', input_kwargs={
+ 'Curve': transform_1,
+ 'Count': 4,
+ 'Radius': multiply_1,
+ 'Limit Radius': True
+ }, attrs={'mode': 'POLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Output': fillet_curve_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_curve_to_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_curve_to_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Profile Curve', None),
+ ('NodeSocketGeometry', 'Shape Curve', None), ('NodeSocketFloat', 'Height', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_1})
+
+ set_curve_tilt = nw.new_node(Nodes.SetCurveTilt, input_kwargs={'Curve': curve_line, 'Tilt': 3.1416})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve,
+ input_kwargs={'Curve': set_curve_tilt, 'Count': 128, 'Length': 0.0500})
+
+ spline_parameter_1 = nw.new_node(Nodes.SplineParameter)
+
+ capture_attribute = nw.new_node(Nodes.CaptureAttribute, input_kwargs={
+ 'Geometry': resample_curve,
+ 2: spline_parameter_1.outputs["Factor"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': capture_attribute.outputs["Geometry"],
+ 'Profile Curve': group_input.outputs["Shape Curve"],
+ 'Fill Caps': True
+ })
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve, input_kwargs={
+ 'Curve': group_input.outputs["Profile Curve"],
+ 'Factor': capture_attribute.outputs[2]
+ }, attrs={'mode': 'FACTOR'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': sample_curve.outputs["Position"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': separate_xyz.outputs["Y"]})
+
+ length = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz}, attrs={'operation': 'LENGTH'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ attribute_statistic = nw.new_node(Nodes.AttributeStatistic, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 2: separate_xyz_1.outputs["Z"]
+ })
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={
+ 'Value': separate_xyz.outputs["Z"],
+ 1: attribute_statistic.outputs["Min"],
+ 2: attribute_statistic.outputs["Max"],
+ 3: multiply,
+ 4: 0.0000
+ })
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': multiply_1,
+ 'Y': multiply_2,
+ 'Z': map_range.outputs["Result"]
+ })
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_to_mesh, 'Position': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': set_position},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_leg_straight', singleton=False, type='GeometryNodeTree')
+def nodegroup_leg_straight(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Profile Curve', None),
+ ('NodeSocketFloat', 'Height', 0.5000), ('NodeSocketInt', 'N-gon', 0),
+ ('NodeSocketFloat', 'Profile Width', 0.5000), ('NodeSocketFloat', 'Aspect Ratio', 0.5000),
+ ('NodeSocketFloat', 'Fillet Ratio', 0.2000), ('NodeSocketInt', 'Resolution', 128)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_1})
+
+ set_curve_tilt = nw.new_node(Nodes.SetCurveTilt, input_kwargs={'Curve': curve_line, 'Tilt': 3.1416})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={
+ 'Curve': set_curve_tilt,
+ 'Count': group_input.outputs["Resolution"],
+ 'Length': 0.0500
+ })
+
+ spline_parameter_1 = nw.new_node(Nodes.SplineParameter)
+
+ capture_attribute = nw.new_node(Nodes.CaptureAttribute, input_kwargs={
+ 'Geometry': resample_curve,
+ 2: spline_parameter_1.outputs["Factor"]
+ })
+
+ tableprofile = nw.new_node(nodegroup_table_profile().name, input_kwargs={
+ 'Profile N-gon': group_input.outputs["N-gon"],
+ 'Profile Width': group_input.outputs["Profile Width"],
+ 'Profile Aspect Ratio': group_input.outputs["Aspect Ratio"],
+ 'Profile Fillet Ratio': group_input.outputs["Fillet Ratio"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': capture_attribute.outputs["Geometry"],
+ 'Profile Curve': tableprofile,
+ 'Fill Caps': True
+ })
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve, input_kwargs={
+ 'Curve': group_input.outputs["Profile Curve"],
+ 'Factor': capture_attribute.outputs[2]
+ }, attrs={'mode': 'FACTOR'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': sample_curve.outputs["Position"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz.outputs["X"], 'Y': separate_xyz.outputs["Y"]})
+
+ length = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz}, attrs={'operation': 'LENGTH'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ attribute_statistic = nw.new_node(Nodes.AttributeStatistic, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 2: separate_xyz_1.outputs["Z"]
+ })
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={
+ 'Value': separate_xyz.outputs["Z"],
+ 1: attribute_statistic.outputs["Min"],
+ 2: attribute_statistic.outputs["Max"],
+ 3: multiply,
+ 4: 0.0000
+ })
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': multiply_1,
+ 'Y': multiply_2,
+ 'Z': map_range.outputs["Result"]
+ })
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_to_mesh, 'Position': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Mesh': set_position, 'Profile Curve': tableprofile},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_curve_board', singleton=False, type='GeometryNodeTree')
+def nodegroup_curve_board(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine,
+ input_kwargs={'Start': (1.0000, 0.0000, -1.0000), 'End': (1.0000, 0.0000, 1.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'Thickness', 0.5000),
+ ('NodeSocketFloat', 'Fillet Radius Vertical', 0.0000), ('NodeSocketFloat', 'width', 0.0000),
+ ('NodeSocketFloat', 'extrude_length', 0.0000)])
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["width"]})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_3})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["width"]})
+
+ curve_line_2 = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_4})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["width"],
+ 'Y': group_input.outputs["extrude_length"]
+ })
+
+ curve_line_3 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_3, 'End': combine_xyz_6})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["extrude_length"],
+ 'Y': group_input.outputs["width"]
+ })
+
+ curve_line_4 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_4, 'End': combine_xyz_5})
+
+ curve_line_5 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_6, 'End': combine_xyz_5})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [curve_line_1, curve_line_2, curve_line_3, curve_line_4, curve_line_5]
+ })
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': join_geometry_1})
+
+ merge_by_distance_1 = nw.new_node(Nodes.MergeByDistance, input_kwargs={'Geometry': curve_to_mesh_1})
+
+ mesh_to_curve = nw.new_node(Nodes.MeshToCurve, input_kwargs={'Mesh': merge_by_distance_1})
+
+ curve_to_board = nw.new_node(nodegroup_curve_to_board().name, input_kwargs={
+ 'Profile Curve': curve_line,
+ 'Shape Curve': mesh_to_curve,
+ 'Height': group_input.outputs["Thickness"]
+ })
+
+ arc = nw.new_node('GeometryNodeCurveArc',
+ input_kwargs={'Resolution': 4, 'Radius': 0.7071, 'Sweep Angle': 4.7124})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': arc.outputs["Curve"],
+ 'Rotation': (0.0000, 0.0000, -0.7854)
+ })
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_2, 'Translation': (0.0000, 0.5000, 0.0000)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': group_input, 'Z': 1.0000})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_3, 'Scale': combine_xyz})
+
+ fillet_curve = nw.new_node('GeometryNodeFilletCurve', input_kwargs={
+ 'Curve': transform_4,
+ 'Count': 8,
+ 'Radius': group_input,
+ 'Limit Radius': True
+ }, attrs={'mode': 'POLY'})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': fillet_curve,
+ 'Rotation': (1.5708, 1.5708, 0.0000),
+ 'Scale': group_input.outputs["Thickness"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Profile Curve': transform_6})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_to_mesh, 'Translation': combine_xyz_1})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_board.outputs["Mesh"], transform_5]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance, input_kwargs={'Geometry': join_geometry})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Thickness"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': merge_by_distance, 'Translation': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_side_leg', singleton=False, type='GeometryNodeTree')
+def nodegroup_side_leg(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine,
+ input_kwargs={'Start': (1.0000, 0.0000, -1.0000), 'End': (1.0000, 0.0000, 1.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'Thickness', 0.5000),
+ ('NodeSocketInt', 'N-gon', 0), ('NodeSocketFloat', 'Profile Width', 0.5000),
+ ('NodeSocketFloat', 'Aspect Ratio', 0.5000), ('NodeSocketFloat', 'Fillet Ratio', 0.2000),
+ ('NodeSocketFloat', 'Fillet Radius Vertical', 0.0000)])
+
+ legstraight = nw.new_node(nodegroup_leg_straight().name, input_kwargs={
+ 'Profile Curve': curve_line,
+ 'Height': group_input.outputs["Thickness"],
+ 'N-gon': group_input.outputs["N-gon"],
+ 'Profile Width': group_input.outputs["Profile Width"],
+ 'Aspect Ratio': group_input.outputs["Aspect Ratio"],
+ 'Fillet Ratio': group_input.outputs["Fillet Ratio"]
+ })
+
+ arc = nw.new_node('GeometryNodeCurveArc',
+ input_kwargs={'Resolution': 4, 'Radius': 0.7071, 'Sweep Angle': 4.7124})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': arc.outputs["Curve"],
+ 'Rotation': (0.0000, 0.0000, -0.7854)
+ })
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_2, 'Translation': (0.0000, 0.5000, 0.0000)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': group_input, 'Z': 1.0000})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_3, 'Scale': combine_xyz})
+
+ fillet_curve = nw.new_node('GeometryNodeFilletCurve', input_kwargs={
+ 'Curve': transform_4,
+ 'Count': 8,
+ 'Radius': group_input,
+ 'Limit Radius': True
+ }, attrs={'mode': 'POLY'})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': fillet_curve,
+ 'Rotation': (1.5708, 1.5708, 0.0000),
+ 'Scale': group_input.outputs["Thickness"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': legstraight.outputs["Profile Curve"],
+ 'Profile Curve': transform_6
+ })
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_to_mesh, 'Translation': combine_xyz_1})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_5, legstraight.outputs["Mesh"]]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance, input_kwargs={'Geometry': join_geometry})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Thickness"]})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': merge_by_distance, 'Translation': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_side_boards', singleton=False, type='GeometryNodeTree')
+def nodegroup_side_boards(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Y', 0.0000), ('NodeSocketFloat', 'Z', 0.0000),
+ ('NodeSocketFloat', 'x1', 0.5000), ('NodeSocketFloat', 'x2', 0.5000),
+ ('NodeSocketFloat', 'x3', 0.0010), ('NodeSocketFloat', 'x4', 0.5000),
+ ('NodeSocketFloat', 'x5', 0.5000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x5"], 1: 0.0000})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': add,
+ 'Y': group_input.outputs["Y"],
+ 'Z': group_input.outputs["Z"]
+ })
+
+ cube = nw.new_node(Nodes.MeshCube,
+ input_kwargs={'Size': combine_xyz, 'Vertices X': 5, 'Vertices Y': 5, 'Vertices Z': 5})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: group_input.outputs["x3"]})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x1"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x2"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Z': subtract})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_1})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x4"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_1, 'Z': subtract_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube, 'Translation': combine_xyz_2})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, transform_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_shelf_boards', singleton=False, type='GeometryNodeTree')
+def nodegroup_shelf_boards(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'Thickness', 0.0100),
+ ('NodeSocketFloat', 'Bottom_z', 0.0000), ('NodeSocketFloat', 'Mid_z', 0.0000),
+ ('NodeSocketFloat', 'Top_z', 0.0000), ('NodeSocketFloat', 'Board_width', 0.3000),
+ ('NodeSocketFloat', 'Leg_gap', 0.5000), ('NodeSocketFloat', 'extrude_length', 0.5000)])
+
+ curve_board = nw.new_node(nodegroup_curve_board().name, input_kwargs={
+ 'Thickness': group_input.outputs["Thickness"],
+ 'Fillet Radius Vertical': 0.0100,
+ 'width': group_input.outputs["Board_width"],
+ 'extrude_length': group_input.outputs["extrude_length"]
+ })
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Leg_gap"], 1: 0.0000})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': group_input.outputs["Bottom_z"]})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': curve_board,
+ 'Translation': combine_xyz_1,
+ 'Rotation': (0.0000, 0.0000, -1.5708)
+ })
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': group_input.outputs["Mid_z"]})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': curve_board,
+ 'Translation': combine_xyz_4,
+ 'Rotation': (0.0000, 0.0000, -1.5708)
+ })
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': group_input.outputs["Top_z"]})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': curve_board,
+ 'Translation': combine_xyz_5,
+ 'Rotation': (0.0000, 0.0000, -1.5708)
+ })
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_1, transform_5, transform_6]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_screw_head', singleton=False, type='GeometryNodeTree')
+def nodegroup_screw_head(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Radius': 0.004, 'Depth': 0.0030})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': cylinder.outputs["Mesh"],
+ 'Rotation': (1.5708, 0.0000, 0.0000)
+ })
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'leg_width', 0.5000),
+ ('NodeSocketFloat', 'board_thickness', 0.5000), ('NodeSocketFloat', 'board_height', 0.5000),
+ ('NodeSocketFloat', 'leg_gap', 0.5000), ('NodeSocketFloat', 'board_width', 0.5000),
+ ('NodeSocketFloat', 'leg_depth', 0.0000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: 0.0000, 1: multiply_1}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_height"], 1: multiply_2})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': subtract, 'Z': add})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Translation': combine_xyz})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_width"], 1: 0.0000})
+
+ divide1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_depth"], 1: 0.5},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: divide1})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': add_2, 'Z': add})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_1})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_gap"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: multiply_3})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: multiply}, attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract_1, 'Y': subtract, 'Z': add})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_2})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_1, transform_2, transform_3]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_shelf_legs', singleton=False, type='GeometryNodeTree')
+def nodegroup_shelf_legs(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'leg_gap', 0.5000),
+ ('NodeSocketFloat', 'leg_curve_ratio', 0.5000), ('NodeSocketFloat', 'leg_width', 0.5000),
+ ('NodeSocketFloat', 'leg_length', 0.5000), ('NodeSocketFloat', 'board_width', 0.5000),
+ ('NodeSocketFloat', 'leg_depth', 0.0000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_width"], 1: 0.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_length"], 1: 0.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["leg_depth"],
+ 1: group_input.outputs["leg_length"]
+ }, attrs={'operation': 'DIVIDE'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_curve_ratio"], 1: 0.0000})
+
+ side_leg = nw.new_node(nodegroup_side_leg().name, input_kwargs={
+ 'Thickness': add,
+ 'N-gon': 4,
+ 'Profile Width': add_1,
+ 'Aspect Ratio': divide,
+ 'Fillet Ratio': add_2,
+ 'Fillet Radius Vertical': add_2
+ })
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: add_1}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': side_leg,
+ 'Translation': combine_xyz,
+ 'Rotation': (0.0000, 1.5708, 0.0000)
+ })
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["board_width"], 1: 0.0000})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: add}, attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_gap"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: multiply_1})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_3})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_3})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': combine_xyz_2})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_4, transform_2, transform_3]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_2},
+ attrs={'is_active_output': True})
+
+
+def geometry_nodes(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ leg_gap = nw.new_node(Nodes.Value, label='leg_gap')
+ leg_gap.outputs[0].default_value = kwargs['leg_board_gap']
+
+ curvature_ratio = nw.new_node(Nodes.Value, label='curvature_ratio')
+ curvature_ratio.outputs[0].default_value = kwargs['leg_curvature_ratio']
+
+ leg_width = nw.new_node(Nodes.Value, label='leg_width')
+ leg_width.outputs[0].default_value = kwargs['leg_width']
+
+ leg_length = nw.new_node(Nodes.Value, label='leg_length')
+ leg_length.outputs[0].default_value = kwargs['leg_length']
+
+ leg_depth = nw.new_node(Nodes.Value, label='leg_depth')
+ leg_depth.outputs[0].default_value = kwargs['leg_depth']
+
+ board_width = nw.new_node(Nodes.Value, label='board_width')
+ board_width.outputs[0].default_value = kwargs['board_width']
+
+ shelf_legs = nw.new_node(nodegroup_shelf_legs().name, input_kwargs={
+ 'leg_gap': leg_gap,
+ 'leg_curve_ratio': curvature_ratio,
+ 'leg_width': leg_width,
+ 'leg_length': leg_length,
+ 'board_width': board_width,
+ 'leg_depth': leg_depth
+ })
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': shelf_legs, 'Material': kwargs['leg_material']})
+
+ board_thickness = nw.new_node(Nodes.Value, label='board_thickness')
+ board_thickness.outputs[0].default_value = kwargs['board_thickness']
+
+ board_extrude_length = nw.new_node(Nodes.Value, label='board_extrude_length')
+ board_extrude_length.outputs[0].default_value = kwargs['board_extrude_length']
+
+ bottom_layer_height = nw.new_node(Nodes.Value, label='bottom_layer_height')
+ bottom_layer_height.outputs[0].default_value = kwargs['bottom_layer_height']
+
+ mid_layer_height = nw.new_node(Nodes.Value, label='mid_layer_height')
+ mid_layer_height.outputs[0].default_value = kwargs['mid_layer_height']
+
+ top_layer_height = nw.new_node(Nodes.Value, label='top_layer_height')
+ top_layer_height.outputs[0].default_value = kwargs['top_layer_height']
+
+ screwhead1 = nw.new_node(nodegroup_screw_head().name, input_kwargs={
+ 'leg_width': leg_width,
+ 'board_thickness': board_thickness,
+ 'board_height': bottom_layer_height,
+ 'leg_gap': leg_gap,
+ 'board_width': board_width,
+ 'leg_depth': leg_depth
+ })
+
+ screwhead2 = nw.new_node(nodegroup_screw_head().name, input_kwargs={
+ 'leg_width': leg_width,
+ 'board_thickness': board_thickness,
+ 'board_height': mid_layer_height,
+ 'leg_gap': leg_gap,
+ 'board_width': board_width,
+ 'leg_depth': leg_depth
+ })
+
+ screwhead3 = nw.new_node(nodegroup_screw_head().name, input_kwargs={
+ 'leg_width': leg_width,
+ 'board_thickness': board_thickness,
+ 'board_height': top_layer_height,
+ 'leg_gap': leg_gap,
+ 'board_width': board_width,
+ 'leg_depth': leg_depth
+ })
+
+ join_geometry2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [screwhead1, screwhead2, screwhead3]})
+
+ set_material_2 = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': join_geometry2,
+ 'Material': get_shelf_material('metal')
+ })
+
+ shelf_boards = nw.new_node(nodegroup_shelf_boards().name, input_kwargs={
+ 'Thickness': board_thickness,
+ 'Bottom_z': bottom_layer_height,
+ 'Mid_z': mid_layer_height,
+ 'Top_z': top_layer_height,
+ 'Board_width': board_width,
+ 'Leg_gap': leg_gap,
+ 'extrude_length': board_extrude_length
+ })
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': shelf_boards, 'Material': kwargs['board_material']})
+
+ side_board_height = nw.new_node(Nodes.Value, label='side_board_height')
+ side_board_height.outputs[0].default_value = kwargs['side_board_height']
+
+ side_boards = nw.new_node(nodegroup_side_boards().name, input_kwargs={
+ 'Y': leg_depth,
+ 'Z': side_board_height,
+ 'x1': side_board_height,
+ 'x2': bottom_layer_height,
+ 'x3': leg_gap,
+ 'x4': top_layer_height,
+ 'x5': board_width
+ })
+
+ set_material_3 = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': side_boards, 'Material': kwargs['leg_material']})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [set_material, set_material_2, set_material_1, set_material_3]
+ })
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry})
+
+ transform4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': realize_instances, 'Scale': (-1, 1, 1)})
+
+ triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': transform4})
+
+ transform5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': triangulate, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform5},
+ attrs={'is_active_output': True})
+
+
+class TriangleShelfBaseFactory(AssetFactory):
+ def __init__(self, factory_seed, params={}, coarse=False):
+ super(TriangleShelfBaseFactory, self).__init__(factory_seed, coarse=coarse)
+ self.params = {}
+
+ def sample_params(self):
+ return self.params.copy()
+
+ def get_asset_params(self, i=0):
+ params = self.sample_params()
+ if params.get('leg_board_gap', None) is None:
+ params['leg_board_gap'] = uniform(0.002, 0.005)
+ if params.get('leg_width', None) is None:
+ params['leg_width'] = uniform(0.01, 0.03)
+ if params.get('leg_depth', None) is None:
+ params['leg_depth'] = uniform(0.01, 0.02)
+ if params.get('leg_length', None) is None:
+ params['leg_length'] = np.clip(normal(0.6, 0.05), 0.45, 0.75)
+ if params.get('leg_curvature_ratio', None) is None:
+ params['leg_curvature_ratio'] = uniform(0.0, 0.02)
+ if params.get('board_thickness', None) is None:
+ params['board_thickness'] = uniform(0.01, 0.025)
+ if params.get('board_width', None) is None:
+ params['board_width'] = np.clip(normal(0.3, 0.03), 0.2, 0.4)
+ if params.get('board_extrude_length', None) is None:
+ params['board_extrude_length'] = uniform(0.03, 0.07)
+ if params.get('side_board_height', None) is None:
+ params['side_board_height'] = uniform(0.02, 0.04)
+ if params.get('bottom_layer_height', None) is None:
+ params['bottom_layer_height'] = uniform(0.05, 0.1)
+ if params.get('shelf_layer_height', None) is None:
+ params['top_layer_height'] = params['leg_length'] - uniform(0.02, 0.07)
+ if params.get('board_material', None) is None:
+ params['board_material'] = np.random.choice(['black_wood', 'wood', 'white'], p=[0.2, 0.6, 0.2])
+ if params.get('leg_material', None) is None:
+ params['leg_material'] = np.random.choice(['black_wood', 'wood', 'white'], p=[0.2, 0.6, 0.2])
+ params['mid_layer_height'] = (params['top_layer_height'] + params['bottom_layer_height']) / 2.
+
+ params = self.get_material_func(params)
+ return params
+
+ def get_material_func(self, params, randomness=True):
+ params['board_material'] = get_shelf_material(params['board_material'])
+ params['leg_material'] = get_shelf_material(params['leg_material'], z_axis_texture=True)
+ return params
+
+ def create_asset(self, i=0, **params):
+ bpy.ops.mesh.primitive_plane_add(size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ obj_params = self.get_asset_params(i)
+ surface.add_geomod(obj, geometry_nodes, attributes=[], input_kwargs=obj_params, apply=True)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+
+class TriangleShelfFactory(TriangleShelfBaseFactory):
+ def sample_params(self):
+ params = dict()
+ params['Dimensions'] = (uniform(0.25, 0.35), uniform(0.25, 0.35), uniform(0.5, 0.7))
+ params['leg_length'] = params['Dimensions'][2]
+ params['board_width'] = params['Dimensions'][0]
+ return params
+
diff --git a/infinigen/assets/shelves/utils.py b/infinigen/assets/shelves/utils.py
new file mode 100644
index 000000000..be55e31e6
--- /dev/null
+++ b/infinigen/assets/shelves/utils.py
@@ -0,0 +1,61 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Beining Han
+
+import bpy
+import numpy as np
+from infinigen.core.util import blender as butil
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler, geometry_node_group_empty_new
+from infinigen.core.nodes import node_utils
+from infinigen.core import tagging, tags as t
+
+from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo
+
+
+def get_nodegroup_assets(func, params):
+ bpy.ops.mesh.primitive_plane_add(
+ size=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ with butil.TemporaryObject(obj) as base_obj:
+ node_group_func = func(**params)
+ geo_outputs = [o for o in node_group_func.outputs if o.bl_socket_idname == 'NodeSocketGeometry']
+ results = {o.name: extract_nodegroup_geo(base_obj, node_group_func, o.name,
+ ng_params={}) for o in geo_outputs}
+
+ return results
+
+@node_utils.to_nodegroup('nodegroup_tagged_cube', singleton=False, type='GeometryNodeTree')
+def nodegroup_tagged_cube(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketVectorTranslation', 'Size', (1.0000, 1.0000, 1.0000))])
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["Size"]})
+
+ index = nw.new_node(Nodes.Index)
+
+ equal = nw.new_node(Nodes.Compare, input_kwargs={2: index, 3: 2}, attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+
+ cube = tagging.tag_nodegroup(nw, cube, t.Subpart.SupportSurface, selection=equal)
+
+ #subdivide_mesh = nw.new_node(Nodes.SubdivideMesh, input_kwargs={'Mesh': cube, 'Level': 2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': cube}, attrs={'is_active_output': True})
+
+
+
+def blender_rotate(vec):
+ if isinstance(vec, tuple):
+ vec = list(vec)
+ if isinstance(vec, list):
+ vec = np.array(vec, dtype=np.float32)
+ if len(vec.shape) == 1:
+ vec = np.expand_dims(vec, axis=-1)
+ if vec.shape[0] == 3:
+ new_vec = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]], dtype=np.float32) @ vec
+ return new_vec.squeeze()
+ if vec.shape[0] == 4:
+ new_vec = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]], dtype=np.float32) @ vec
+ return new_vec.squeeze()
diff --git a/infinigen/assets/small_plants/fern.py b/infinigen/assets/small_plants/fern.py
index ab15536f3..b824a2560 100644
--- a/infinigen/assets/small_plants/fern.py
+++ b/infinigen/assets/small_plants/fern.py
@@ -21,7 +21,7 @@
from infinigen.core import surface
from infinigen.assets.materials import simple_greenery
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
def random_pinnae_level2_curvature():
z_max_curvature = uniform(0.3, 0.45, (1,))[0]
diff --git a/infinigen/assets/small_plants/leaf_general.py b/infinigen/assets/small_plants/leaf_general.py
index 2305e6f62..8f3222f7a 100644
--- a/infinigen/assets/small_plants/leaf_general.py
+++ b/infinigen/assets/small_plants/leaf_general.py
@@ -16,7 +16,7 @@
C = bpy.context
D = bpy.data
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class LeafFactory(AssetFactory):
diff --git a/infinigen/assets/small_plants/leaf_heart.py b/infinigen/assets/small_plants/leaf_heart.py
index e4efb4b9c..83cc15249 100644
--- a/infinigen/assets/small_plants/leaf_heart.py
+++ b/infinigen/assets/small_plants/leaf_heart.py
@@ -12,7 +12,7 @@
C = bpy.context
D = bpy.data
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class LeafHeartFactory(AssetFactory):
scale = 0.2
diff --git a/infinigen/assets/small_plants/num_leaf_grass.py b/infinigen/assets/small_plants/num_leaf_grass.py
index 1a81f994d..567faabfe 100644
--- a/infinigen/assets/small_plants/num_leaf_grass.py
+++ b/infinigen/assets/small_plants/num_leaf_grass.py
@@ -16,7 +16,7 @@
from infinigen.assets.small_plants.leaf_heart import LeafHeartFactory
from infinigen.assets.materials import simple_greenery
import numpy as np
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_leafon_stem', singleton=False, type='GeometryNodeTree')
def nodegroup_leaf_on_stem(nw: NodeWrangler, z_rotation=(0, 0, 0,), leaf_scale=1.0, leaf=None):
diff --git a/infinigen/assets/small_plants/snake_plant.py b/infinigen/assets/small_plants/snake_plant.py
index 917c8b5fc..b620e5a8c 100644
--- a/infinigen/assets/small_plants/snake_plant.py
+++ b/infinigen/assets/small_plants/snake_plant.py
@@ -14,7 +14,7 @@
from infinigen.core.placement.factory import AssetFactory
import numpy as np
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_pedal_thickness', singleton=False, type='GeometryNodeTree')
def nodegroup_pedal_thickness(nw: NodeWrangler):
diff --git a/infinigen/assets/small_plants/spider_plant.py b/infinigen/assets/small_plants/spider_plant.py
index 8a5cf8f47..3fc59c306 100644
--- a/infinigen/assets/small_plants/spider_plant.py
+++ b/infinigen/assets/small_plants/spider_plant.py
@@ -16,7 +16,7 @@
import numpy as np
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_set_leaf_countour', singleton=False, type='GeometryNodeTree')
def nodegroup_set_leaf_countour(nw: NodeWrangler):
diff --git a/infinigen/assets/small_plants/succulent.py b/infinigen/assets/small_plants/succulent.py
index 31d081a46..6677108f1 100644
--- a/infinigen/assets/small_plants/succulent.py
+++ b/infinigen/assets/small_plants/succulent.py
@@ -16,7 +16,7 @@
import numpy as np
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_pedal_cross_contour_top', singleton=False, type='GeometryNodeTree')
def nodegroup_pedal_cross_contour_top(nw: NodeWrangler):
diff --git a/infinigen/assets/table_decorations/__init__.py b/infinigen/assets/table_decorations/__init__.py
new file mode 100644
index 000000000..a58696a3a
--- /dev/null
+++ b/infinigen/assets/table_decorations/__init__.py
@@ -0,0 +1,3 @@
+from .vase import VaseFactory
+from .sink import SinkFactory, TapFactory
+from .book import BookFactory, BookColumnFactory, BookStackFactory
diff --git a/infinigen/assets/table_decorations/book.py b/infinigen/assets/table_decorations/book.py
new file mode 100644
index 000000000..785abedce
--- /dev/null
+++ b/infinigen/assets/table_decorations/book.py
@@ -0,0 +1,215 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+import math
+import trimesh
+from numpy.random import uniform
+from trimesh import proximity
+
+from infinigen.assets.materials import text
+from infinigen.assets.utils.decorate import read_co, write_attribute, write_co
+from infinigen.assets.utils.object import center, join_objects, new_bbox, new_cube, obj2trimesh
+from infinigen.assets.utils.mesh import longest_ray
+from infinigen.assets.utils.uv import wrap_front_back_side
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+from infinigen.assets.material_assignments import AssetList
+
+class BookFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BookFactory, self).__init__(factory_seed, coarse)
+ self.rel_scale = log_uniform(1, 1.5)
+ self.skewness = log_uniform(1.3, 1.8)
+ self.unit = .0127
+ self.is_paperback = uniform() < .5
+ self.margin = uniform(.005, .01)
+ self.offset = 0 if uniform() < .5 else log_uniform(.002, .008)
+ self.thickness = uniform(.002, .003)
+
+ materials = AssetList['BookFactory']()
+ self.surface = materials['surface'].assign_material()
+ self.cover_surface = materials['cover_surface'].assign_material()
+ if self.cover_surface == text.Text:
+ self.cover_surface = self.cover_surface(self.factory_seed)
+
+ scratch_prob, edge_wear_prob = materials['wear_tear_prob']
+ self.scratch, self.edge_wear = materials['wear_tear']
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.texture_shared = uniform() < .2
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ width = int(log_uniform(.08, .15) * self.rel_scale / self.unit) * self.unit
+ height = int(width * self.skewness / self.unit) * self.unit
+ depth = uniform(.01, .02) * self.rel_scale
+ fn = self.make_paperback if self.is_paperback else self.make_hardcover
+ # noinspection PyArgumentList
+ obj = fn(width, height, depth)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+ def make_paperback(self, width, height, depth):
+ paper = self.make_paper(depth, height, width)
+ obj = new_cube()
+ obj.location = width / 2, height / 2, depth / 2
+ obj.scale = width / 2, height / 2, depth / 2
+ butil.apply_transform(obj, True)
+
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = []
+ for e in bm.edges:
+ u, v = e.verts
+ if u.co[0] > 0 and v.co[0] > 0 and u.co[-1] != v.co[-1]:
+ geom.append(e)
+ bmesh.ops.delete(bm, geom=geom, context='EDGES')
+
+ self.make_cover(obj)
+ write_attribute(obj, 1, 'cover', 'FACE')
+ obj = join_objects([paper, obj])
+ return obj
+
+ def make_paper(self, depth, height, width):
+ paper = new_cube()
+ paper.location = width / 2, height / 2, depth / 2
+ paper.scale = width / 2 - 1e-4, height / 2, depth / 2 - 1e-4
+ butil.apply_transform(paper, True)
+ self.surface.apply(paper)
+ return paper
+
+ def make_hardcover(self, width, height, depth):
+ paper = self.make_paper(depth, height, width)
+ obj = new_cube()
+ count = 8
+ butil.modify_mesh(obj, 'ARRAY', count=count, relative_offset_displace=(0, 0, 1),
+ use_merge_vertices=True)
+ obj.location = 1, 1, 1
+ butil.apply_transform(obj, loc=True)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = []
+ for v in bm.verts:
+ if v.co[0] > 0 and 0 < v.co[-1] < count * 2:
+ geom.append(v)
+ bmesh.ops.delete(bm, geom=geom, context='VERTS')
+ obj.location = 0, - self.margin, 0
+ obj.scale = (width + self.margin) / 2, height / 2 + self.margin, depth / 2 / count
+ butil.apply_transform(obj, True)
+ x, y, z = read_co(obj).T
+ ratio = np.minimum(z / depth, 1 - z / depth)
+ x -= 4 * ratio * (1 - ratio) * self.offset
+ write_co(obj, np.stack([x, y, z]).T)
+ self.make_cover(obj)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ write_attribute(obj, 1, 'cover', 'FACE')
+ obj = join_objects([paper, obj])
+ return obj
+
+ def make_cover(self, obj):
+ obj.rotation_euler[0] = np.pi / 2
+ butil.apply_transform(obj)
+ wrap_front_back_side(obj, self.cover_surface, self.texture_shared)
+ obj.rotation_euler[0] = -np.pi / 2
+ butil.apply_transform(obj)
+
+
+class BookColumnFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BookColumnFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.base_factories = [BookFactory(np.random.randint(1e5)) for _ in range(np.random.randint(1, 4))]
+ self.n_books = np.random.randint(10, 20)
+ self.max_angle = uniform(0, np.pi / 9) if uniform() < .7 else 0
+ self.max_rel_scale = max(f.rel_scale for f in self.base_factories)
+ self.max_skewness = max(f.skewness for f in self.base_factories)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ height = .15 * self.max_rel_scale * self.max_skewness
+ return new_bbox(0, (.02 + np.sin(self.max_angle) * height) * self.n_books * self.max_rel_scale,
+ -.15 * self.max_rel_scale, 0, 0, height)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ books = []
+ for i in range(self.n_books):
+ factory = np.random.choice(self.base_factories)
+ obj = factory.create_asset(i=i)
+ x, y, z = read_co(obj).T
+ obj.location = [-np.max(x), -np.min(y), -np.min(z)]
+ butil.apply_transform(obj, True)
+ if uniform() < .5:
+ obj.rotation_euler = np.pi / 2 - uniform(0, self.max_angle), 0, np.pi / 2
+ else:
+ obj.location[-1] = -np.max(z)
+ butil.apply_transform(obj, True)
+ obj.rotation_euler = np.pi / 2 + uniform(0, self.max_angle), 0, np.pi / 2
+ butil.apply_transform(obj)
+ if i > 0:
+ obj.location[0] = 10
+ butil.apply_transform(obj, True)
+ dist = longest_ray(books[-1], obj, (-1, 0, 0))
+ dist_ = longest_ray(obj, books[-1], (1, 0, 0))
+ offset = np.minimum(np.min(dist), np.min(dist_))
+ obj.location[0] = -offset
+ butil.apply_transform(obj, True)
+ books.append(obj)
+ obj = join_objects(books)
+ obj.location[0] = -np.min(read_co(obj)[:, 0])
+ butil.apply_transform(obj, True)
+ return obj
+
+def rotate(theta, x, y):
+ return x * math.cos(theta) - y * math.sin(theta), x * math.sin(theta) + y * math.cos(theta)
+
+class BookStackFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(BookStackFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.base_factories = [BookFactory(np.random.randint(1e5)) for _ in range(np.random.randint(1, 4))]
+ self.n_books = int(log_uniform(5, 15))
+ self.max_angle = uniform(np.pi / 9, np.pi / 6) if uniform() < .7 else 0
+ self.max_rel_scale = max(f.rel_scale for f in self.base_factories)
+ self.max_skewness = max(f.skewness for f in self.base_factories)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ x_lo = -.15 * self.max_rel_scale / 2
+ x_hi = .15 * self.max_rel_scale / 2
+ y_lo = -.15 * self.max_rel_scale / 2 * self.max_skewness
+ y_hi = .15 * self.max_rel_scale / 2 * self.max_skewness
+
+ theta = self.max_angle
+ x_1, y_1 = rotate(theta, x_lo, y_lo)
+ x_2, y_2 = rotate(theta, x_lo, y_hi)
+ x_3, y_3 = rotate(theta, x_hi, y_lo)
+ x_4, y_4 = rotate(theta, x_hi, y_hi)
+
+ return new_bbox(min(min([x_1,x_2,x_3,x_4]), x_lo ), max(max([x_1,x_2,x_3,x_4]), x_hi),
+ min(min([y_1,y_2,y_3,y_4]), y_lo), max(max([y_1,y_2,y_3,y_4]), y_hi),
+ 0, self.n_books * .02 * self.max_rel_scale * 0.8)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ books = []
+ offset = 0
+ for i in range(self.n_books):
+ factory = np.random.choice(self.base_factories)
+ obj = factory.create_asset(i=i)
+ c = center(obj)[:-1]
+ obj.location = -c[0], -c[1], offset - np.min(read_co(obj)[:, -1])
+ obj.rotation_euler[-1] = uniform(-self.max_angle, self.max_angle)
+ butil.apply_transform(obj, True)
+ offset = np.max(read_co(obj)[:, -1])
+ books.append(obj)
+ return join_objects(books)
diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py
new file mode 100644
index 000000000..343554bb2
--- /dev/null
+++ b/infinigen/assets/table_decorations/sink.py
@@ -0,0 +1,785 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Hongyu Wen: sink geometry
+# - Meenal Parakh: material assignment
+# - Stamatis Alexandropoulos: taps
+# - Alexander Raistrick: placeholder, optimize detail, redo cutter
+
+import random
+
+import bpy
+import mathutils
+
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.assets.utils import bbox_from_mesh
+from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core import tagging, tags as t
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.material_assignments import AssetList
+
+
+class SinkFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], upper_height=None):
+ super(SinkFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+ self.factory_seed = factory_seed
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions, upper_height=upper_height)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+ self.params.update(self.material_params)
+
+ self.tap_factory = TapFactory(factory_seed)
+
+ def get_material_params(self):
+ material_assignments = AssetList['SinkFactory']()
+ params = {
+ "Sink": material_assignments['sink'].assign_material(),
+ "Tap": material_assignments['tap'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = U() < scratch_prob
+ is_edge_wear = U() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions, upper_height, use_default=False, open=False):
+ width = U(0.4, 1.0)
+ depth = U(0.4, 0.5)
+ curvature = U(1.0, 1.0)
+ if upper_height is None:
+ upper_height = U(0.2, 0.4)
+ lower_height = U(0.00, 0.01)
+ hole_radius = U(0.02, 0.05)
+ margin = U(0.02, 0.05)
+ watertap_margin = U(0.1, 0.12)
+
+ params = {
+ 'Width': width,
+ 'Depth': depth,
+ 'Curvature': curvature,
+ 'Upper Height': upper_height,
+ 'Lower Height': lower_height,
+ 'HoleRadius': hole_radius,
+ 'Margin': margin,
+ 'WaterTapMargin': watertap_margin,
+ 'ProtrudeAboveCounter': U(0.01, 0.025),
+ }
+ return params
+
+ def _extract_geo_results(self):
+
+ params = self.params.copy()
+ params.pop('ProtrudeAboveCounter')
+
+ with butil.TemporaryObject(butil.spawn_vert()) as temp:
+ obj = extract_nodegroup_geo(
+ temp, nodegroup_sink_geometry(), 'Geometry', ng_params=params
+ )
+ cutter = extract_nodegroup_geo(
+ temp, nodegroup_sink_geometry(), 'Cutter', ng_params=params
+ )
+
+ return obj, cutter
+
+ def create_placeholder(self, i, **kwargs) -> bpy.types.Object:
+
+ obj, cutter = self._extract_geo_results()
+ butil.delete(cutter)
+
+ min_corner, max_corner = butil.bounds(obj)
+ min_corner[-1] = max_corner[-1] - self.params['ProtrudeAboveCounter']
+ top_slice_placeholder = bbox_from_mesh.box_from_corners(min_corner, max_corner)
+
+ butil.delete(obj)
+
+ return top_slice_placeholder
+
+ def create_asset(self,i, placeholder, state=None, **params):
+
+ obj, cutter = self._extract_geo_results()
+ tagging.tag_system.relabel_obj(obj)
+
+ cutter.parent = obj
+ cutter.name = repr(self) + f'.spawn_placeholder({i}).cutter'
+ cutter.hide_render = True
+
+ tap_loc = (-self.params['Depth'] / 2, 0, self.params['Upper Height'])
+ tap = self.tap_factory.spawn_asset(i, loc=tap_loc, rot=(0,0,0))
+ tap.parent = obj
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+class TapFactory(AssetFactory):
+
+ def __init__(self, factory_seed):
+ super().__init__(factory_seed)
+ with FixedSeed(factory_seed):
+ self.params, self.scratch, self.edge_wear = self.get_material_params()
+
+
+ @staticmethod
+ def tap_parameters():
+ params = {
+ 'base_width' : U(0.570,0.630),
+ 'tap_head': U(0.7,1.1),
+ 'roation_z': U(5.5,7.0),
+ 'tap_height': U(0.5,1),
+ 'base_radius': U(0.0,0.3),
+ 'Switch': True if U()>0.5 else False,
+ 'Y': U(-0.5, -0.06),
+ 'hand_type': True if U()>0.2 else False,
+ 'hands_length_x': U(0.750,1.25),
+ 'hands_length_Y': U(0.950, 1.550),
+ 'one_side': True if U()>0.5 else False,
+ 'different_type': True if U()>0.8 else False
+ }
+ return params
+
+
+ def get_material_params(self):
+ material_assignments = AssetList['TapFactory']()
+ tap_material = material_assignments['tap'].assign_material()
+
+ wrapped_params = {
+ 'Tap': surface.shaderfunc_to_material(tap_material)
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = U() < scratch_prob
+ is_edge_wear = U() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ def create_asset(self, **_):
+ obj = butil.spawn_cube()
+ butil.modify_mesh(obj, 'NODES', node_group=nodegroup_water_tap(), ng_inputs=self.params, apply=True)
+ obj.scale = (0.4,)*3
+ obj.rotation_euler.z += np.pi
+ butil.apply_transform(obj)
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+@node_utils.to_nodegroup('nodegroup_handle', singleton=False, type='GeometryNodeTree')
+def nodegroup_handle(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment, input_kwargs={
+ 'Start': (0.0000, 0.0000, 0.0000),
+ 'Start Handle': (0.0000, 0.0000, 0.7000),
+ 'End Handle': (0.2000, 0.0000, 0.7000),
+ 'End': (1.0000, 0.0000, 0.9000)
+ })
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], [(0.0000, 0.9750), (1.0000, 0.1625)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: float_curve, 1: 1.3000},
+ attrs={'operation': 'MULTIPLY'})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius,
+ input_kwargs={'Curve': bezier_segment, 'Radius': multiply})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.2000})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': set_curve_radius,
+ 'Profile Curve': curve_circle.outputs["Curve"],
+ 'Fill Caps': True
+ })
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': separate_xyz.outputs["X"], 1: 0.2000, 3: 1.0000, 4: 2.5000})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz.outputs["Y"], 1: map_range.outputs["Result"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': separate_xyz.outputs["X"],
+ 'Y': multiply_1,
+ 'Z': separate_xyz.outputs["Z"]
+ })
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_to_mesh, 'Position': combine_xyz})
+
+ subdivision_surface = nw.new_node(Nodes.SubdivisionSurface, input_kwargs={'Mesh': set_position, 'Level': 2})
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': subdivision_surface})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_water_tap', singleton=False, type='GeometryNodeTree')
+def nodegroup_water_tap(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'base_width', U(0.2,0.3)),
+ ('NodeSocketFloat', 'tap_head', U(0.7,1.1)),
+ ('NodeSocketFloat', 'roation_z',U(5.5,7.0)),
+ ('NodeSocketFloat', 'tap_height', U(0.5,1)),
+ ('NodeSocketFloatDistance', 'base_radius', U(0.0,0.1)),
+ ('NodeSocketBool', 'Switch',True if U()>0.5 else False),
+ ('NodeSocketFloat', 'Y', U(-0.5, -0.06)),
+ ('NodeSocketBool', 'hand_type', True if U()>0.2 else False),
+ ('NodeSocketFloat', 'hands_length_x', U(0.750,1.25)),
+ ('NodeSocketFloat', 'hands_length_Y', U(0.950, 1.550)),
+ ('NodeSocketBool', 'one_side', True if U()>0.5 else False),
+ ('NodeSocketBool', 'different_type', True if U()>0.8 else False),
+ ('NodeSocketBool', 'length_one_side', True if U()>0.8 else False)])
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketMaterial', 'Tap', None)])
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.0500})
+
+ fill_curve_1 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': curve_circle.outputs["Curve"]})
+
+ extrude_mesh_1 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve_1, 'Offset Scale': 0.1500})
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': 0.2000, 'Height': 0.7000})
+
+ fillet_curve = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': quadrilateral, 'Count': 19, 'Radius': 0.1000},
+ attrs={'mode': 'POLY'})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': fillet_curve})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve, 'Offset Scale': 0.0500})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': (0.0000, 0.0000, 0.6000)})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.0300})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle_1.outputs["Curve"]})
+
+ curve_circle_2 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.2000})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_2.outputs["Curve"], 'Translation': (0.0000, 0.2000, 0.0000)})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_geometry, 'Rotation': (-1.5708, 1.5708, 0.0000), 'Scale': (1.0000, 0.7000, 1.0000)})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 0.2000, 'Y': group_input.outputs["Y"]})
+
+ bezier_segment = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Resolution': 177, 'Start': (0.0000, 0.0000, 0.0000), 'Start Handle': (0.0000, 1.2000, 0.0000), 'End Handle': combine_xyz_3, 'End': (-0.0500, 0.1000, 0.0000)})
+
+ trim_curve = nw.new_node(Nodes.TrimCurve, input_kwargs={'Curve': bezier_segment, 3: 0.6625, 5: 3.0000})
+
+ transform_geometry_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': trim_curve, 'Rotation': (1.5708, 0.0000, 2.5220), 'Scale': (5.2000, 0.5000, 7.8000)})
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.0300})
+
+ curve_to_mesh_2 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': transform_geometry_6, 'Profile Curve': curve_circle_3.outputs["Curve"]})
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["Switch"], 14: transform_geometry_1, 15: curve_to_mesh_2})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': switch.outputs[6], 'Profile Curve': curve_circle_1.outputs["Curve"]})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ greater_than = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: -0.0100}, attrs={'operation': 'GREATER_THAN'})
+
+ switch_1 = nw.new_node(Nodes.Switch,
+ input_kwargs={0: group_input.outputs["Switch"], 2: greater_than, 3: 1.0000},
+ attrs={'input_type': 'FLOAT'})
+
+ separate_geometry = nw.new_node(Nodes.SeparateGeometry,
+ input_kwargs={'Geometry': curve_to_mesh_1, 'Selection': switch_1.outputs["Output"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': 1.0000, 'Z': group_input.outputs["tap_head"]})
+
+ switch_2 = nw.new_node(Nodes.Switch,
+ input_kwargs={0: group_input.outputs["Switch"], 8: combine_xyz, 9: (1.0000, 1.0000, 1.0000)},
+ attrs={'input_type': 'VECTOR'})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': separate_geometry.outputs["Selection"], 'Translation': (0.0000, 0.0000, 0.6000), 'Scale': switch_2.outputs[3]})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [curve_to_mesh, transform_geometry_2]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["roation_z"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': 1.0000, 'Z': group_input.outputs["tap_height"]})
+
+ transform_geometry_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry, 'Rotation': combine_xyz_1, 'Scale': combine_xyz_2})
+
+ handle = nw.new_node(nodegroup_handle().name)
+
+ transform_geometry_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': handle, 'Translation': (0.0000, -0.2000, 0.0000), 'Rotation': (0.0000, 0.0000, 3.6652), 'Scale': (0.3000, 0.3000, 0.3000)})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': handle, 'Translation': (0.0000, 0.2000, 0.0000), 'Rotation': (0.0000, 0.0000, 2.6180), 'Scale': (0.3000, 0.3000, 0.3000)})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_geometry_4, transform_geometry_3]})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 41, 'Side Segments': 39, 'Radius': 0.0300, 'Depth': 0.1000})
+
+ transform_geometry_7 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': (0.0000, 0.0500, 0.1000), 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ switch_5 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["one_side"], 14: transform_geometry_7})
+
+ transform_geometry_8 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': (0.0000, -0.0500, 0.1000), 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [switch_5.outputs[6], transform_geometry_8]})
+
+ cylinder_1 = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': 41, 'Side Segments': 39, 'Radius': 0.0050, 'Depth': 0.1000})
+
+ transform_geometry_9 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder_1.outputs["Mesh"], 'Translation': (0.0000, 0.0800, 0.1500), 'Scale': (1.0000, 1.0000, 1.1000)})
+
+ switch_4 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["one_side"], 14: transform_geometry_9})
+
+ transform_geometry_10 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder_1.outputs["Mesh"], 'Translation': (0.0000, -0.0800, 0.1500), 'Rotation': (0.0000, 0.0000, 0.0855), 'Scale': (1.0000, 1.0000, 1.1000)})
+
+ transform_geometry_17 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_geometry_10, 'Translation': (0.0000, -0.0100, -0.0050), 'Scale': (4.1000, 1.0000, 1.0000)})
+
+ switch_8 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["length_one_side"], 14: transform_geometry_10, 15: transform_geometry_17})
+
+ switch_7 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["one_side"], 14: transform_geometry_10, 15: switch_8.outputs[6]})
+
+ join_geometry_4 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [switch_4.outputs[6], switch_7.outputs[6]]})
+
+ join_geometry_5 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [join_geometry_3, join_geometry_4]})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["hands_length_x"], 'Y': group_input.outputs["hands_length_Y"], 'Z': 1.0000})
+
+ transform_geometry_11 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_5, 'Scale': combine_xyz_4})
+
+ switch_3 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["hand_type"], 14: join_geometry_2, 15: transform_geometry_11})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.0500})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': curve_circle.outputs["Curve"]})
+
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve, 'Offset Scale': 0.1500})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_geometry_5, switch_3.outputs[6], extrude_mesh.outputs["Mesh"]]})
+
+ bezier_segment_1 = nw.new_node(Nodes.CurveBezierSegment,
+ input_kwargs={'Resolution': 54, 'Start': (0.0000, 0.0000, 0.0000), 'Start Handle': (0.0000, 0.0000, 0.7000), 'End Handle': (0.2000, 0.0000, 0.7000), 'End': (1.0000, 0.0000, 0.9000)})
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], [(0.0000, 0.9750), (0.6295, 0.4125), (1.0000, 0.1625)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: float_curve, 1: 1.3000}, attrs={'operation': 'MULTIPLY'})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': bezier_segment_1, 'Radius': multiply})
+
+ curve_circle_4 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': 0.1000})
+
+ curve_to_mesh_3 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': set_curve_radius, 'Profile Curve': curve_circle_4.outputs["Curve"], 'Fill Caps': True})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': separate_xyz_1.outputs["X"], 1: 0.2000, 3: 1.0000, 4: 2.5000})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: map_range.outputs["Result"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': separate_xyz_1.outputs["X"], 'Y': multiply_1, 'Z': separate_xyz_1.outputs["Z"]})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_to_mesh_3, 'Position': combine_xyz_5, 'Offset': (0.0000, 0.0000, 0.0000)})
+
+ subdivision_surface = nw.new_node(Nodes.SubdivisionSurface, input_kwargs={'Mesh': set_position, 'Level': 1})
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': subdivision_surface})
+
+ transform_geometry_12 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': set_shade_smooth, 'Translation': (0.0000, 0.0000, 0.1000), 'Rotation': (0.0000, 0.0000, 0.6807), 'Scale': (0.4000, 0.4000, 0.3000)})
+
+ curve_circle_5 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': 307, 'Radius': 0.0550})
+
+ fill_curve_2 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': curve_circle_5.outputs["Curve"]})
+
+ extrude_mesh_2 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve_2, 'Offset Scale': 0.1500})
+
+ cylinder_2 = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Vertices': 100, 'Radius': 0.0100, 'Depth': 0.7000})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': cylinder_2.outputs["Mesh"]})
+
+ transform_geometry_13 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': set_position_1, 'Translation': (0.3000, 0.0000, 0.2500), 'Rotation': (0.0000, -2.0420, 0.0000), 'Scale': (1.7000, 3.1000, 1.0000)})
+
+ cylinder_3 = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Vertices': 318, 'Radius': 0.0200, 'Depth': 0.0300})
+
+ transform_geometry_14 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cylinder_3.outputs["Mesh"], 'Translation': (0.5950, 0.0000, 0.3800)})
+
+ join_geometry_7 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_geometry_13, transform_geometry_14]})
+
+ transform_geometry_15 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_7, 'Scale': (0.9000, 1.0000, 1.0000)})
+
+ join_geometry_8 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_geometry_12, extrude_mesh_2.outputs["Mesh"], transform_geometry_15]})
+
+ transform_geometry_16 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_8, 'Rotation': (0.0000, 0.0000, 3.1416)})
+
+ switch_6 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["different_type"], 14: join_geometry_1, 15: transform_geometry_16})
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': group_input.outputs["base_width"], 'Height': 0.7000})
+
+ fillet_curve = nw.new_node(Nodes.FilletCurve,
+ input_kwargs={'Curve': quadrilateral, 'Count': 19, 'Radius': group_input.outputs["base_radius"]},
+ attrs={'mode': 'POLY'})
+
+ fill_curve_1 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': fillet_curve})
+
+ extrude_mesh_1 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve_1, 'Offset Scale': 0.0500})
+
+ join_geometry_6 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [switch_6.outputs[6], extrude_mesh_1.outputs["Mesh"]]})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': join_geometry_6,
+ 'Material': group_input.outputs["Tap"]
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_sink_geometry', singleton=False, type='GeometryNodeTree')
+def nodegroup_sink_geometry(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Depth', 2.0000), ('NodeSocketFloat', 'Curvature', 0.9500),
+ ('NodeSocketFloat', 'Upper Height', 1.0000), ('NodeSocketFloat', 'Lower Height', -0.0500),
+ ('NodeSocketFloatDistance', 'HoleRadius', 0.1000), ('NodeSocketFloat', 'Margin', 0.5000),
+ ('NodeSocketFloat', 'WaterTapMargin', 0.5000),
+ ('NodeSocketMaterial', 'Tap', None),
+ ('NodeSocketMaterial', 'Sink', None),])
+
+ reroute_3 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Depth"]})
+
+ reroute_2 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Width"]})
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': reroute_3, 'Height': reroute_2})
+
+ minimum = nw.new_node(Nodes.Math, input_kwargs={0: reroute_3, 1: reroute_2}, attrs={'operation': 'MINIMUM'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: minimum, 1: 0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ # inside of sink curve
+ sink_interior_border = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': quadrilateral, 'Count': 50, 'Radius': multiply},
+ attrs={'mode': 'POLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Curvature"],
+ 'Y': group_input.outputs["Curvature"]
+ })
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': sink_interior_border, 'Scale': combine_xyz_1})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["HoleRadius"]})
+
+ join_geometry_4 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_1, curve_circle.outputs["Curve"]]})
+
+ fill_curve_1 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': join_geometry_4})
+
+ #fill_curve_1 = tagging.tag_nodegroup(nw, fill_curve_1, t.Subpart.SupportSurface)
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Lower Height"]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': reroute})
+
+ transform_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': fill_curve_1, 'Translation': combine_xyz_2})
+
+ extrude_mesh_2 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={
+ 'Mesh': transform_2,
+ 'Offset Scale': -0.0100,
+ 'Individual': False
+ })
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': curve_circle.outputs["Curve"],
+ 'Scale': (0.7000, 0.7000, 1.0000)
+ })
+
+ join_geometry_6 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_circle.outputs["Curve"], transform_5]})
+
+ fill_curve_4 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': join_geometry_6})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: reroute, 1: -0.0100})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ transform_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': fill_curve_4, 'Translation': combine_xyz_4})
+
+ extrude_mesh_4 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={
+ 'Mesh': transform_6,
+ 'Offset Scale': group_input.outputs["Lower Height"],
+ 'Individual': False
+ })
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Lower Height"], 1: -0.0100})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_6})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': curve_line,
+ 'Profile Curve': curve_circle.outputs["Curve"]
+ })
+
+ transform_7 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_to_mesh, 'Translation': combine_xyz_2})
+
+ join_geometry_5 = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [extrude_mesh_2.outputs["Mesh"], transform_2, extrude_mesh_4.outputs["Mesh"], transform_7]
+ })
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': sink_interior_border, 'Scale': (0.9900, 0.9900, 1.0000)})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform, sink_interior_border]})
+
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': join_geometry})
+
+ extrude_mesh_1 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={
+ 'Mesh': fill_curve,
+ 'Offset Scale': group_input.outputs["Lower Height"]
+ })
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ less_than = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: 0.0000},
+ attrs={'operation': 'LESS_THAN'})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["X"], 1: group_input.outputs["Curvature"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_1.outputs["Y"], 1: group_input.outputs["Curvature"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_1, 'Y': multiply_2, 'Z': separate_xyz_1.outputs["Z"]})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': extrude_mesh_1.outputs["Mesh"],
+ 'Selection': less_than,
+ 'Position': combine_xyz
+ })
+
+ add_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["Margin"]})
+
+ add_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["Margin"]})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: group_input.outputs["WaterTapMargin"]})
+
+ quadrilateral_1 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': add_4, 'Height': add_2})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["WaterTapMargin"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3})
+
+ transform_8 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': quadrilateral_1, 'Translation': combine_xyz_7})
+
+ fillet_curve_1 = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': transform_8, 'Count': 10, 'Radius': multiply},
+ attrs={'mode': 'POLY'})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [sink_interior_border, fillet_curve_1]})
+
+ fill_curve_2 = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': join_geometry_2})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Lower Height"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Upper Height"], 1: multiply_4})
+
+ extrude_mesh_3 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve_2, 'Offset Scale': add_5})
+
+ reroute_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Lower Height"]})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': reroute_1})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': extrude_mesh_3.outputs["Mesh"],
+ 'Translation': combine_xyz_3
+ })
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': transform_3})
+
+ #watertap = nw.new_node(nodegroup_water_tap().name, input_kwargs={'Tap': group_input.outputs['Tap']})
+
+ add_6 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Depth"],
+ 1: group_input.outputs["WaterTapMargin"]
+ })
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: add_6, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_5, 'Z': group_input.outputs["Upper Height"]})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={
+ 'Geometry': [join_geometry_5, set_position, join_geometry_3]#, transform_geometry]
+ })
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': join_geometry_1,
+ 'Material': group_input.outputs["Sink"]
+ })
+
+ add_7 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["WaterTapMargin"], 1: group_input.outputs["Margin"]})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: add_7, 1: 2.5600}, attrs={'operation': 'DIVIDE'})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': divide})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': set_material, 'Offset': combine_xyz_8})
+
+ # region CREATE CUTTER (manually added by araistrick post-fact)
+
+ sink_interior_border_simplified = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={
+ 'Curve': quadrilateral, 'Count': 3, 'Radius': multiply
+ },
+ attrs={'mode': 'POLY'}
+ )
+
+ scaled_sink_interior_border = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': sink_interior_border_simplified,
+ 'Scale': (1.01, 1.01, 1) #scale it up just a little to avoid zclip
+ })
+
+ fill_interior = nw.new_node(
+ Nodes.FillCurve,
+ input_kwargs={'Curve': scaled_sink_interior_border},
+ attrs={'mode': 'NGONS'}
+ )
+
+ extrude_amt = nw.scalar_add(
+ group_input.outputs["Lower Height"],
+ group_input.outputs["Upper Height"],
+ 0.05
+ )
+ extrude = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={
+ 'Mesh': fill_interior,
+ 'Offset Scale': extrude_amt
+ })
+
+ # same translation as set_position_1, to keep it in sync
+ setpos_move_cutter = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': extrude, 'Offset': combine_xyz_8})
+
+ # endregion
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={
+ 'Geometry': set_position_1,
+ 'Cutter': setpos_move_cutter
+ })
+
+
+
+def geometry_node_to_bbox(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': bounding_box, 'Scale': (0.100, 0.100, 0.1000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/table_decorations/utils.py b/infinigen/assets/table_decorations/utils.py
new file mode 100644
index 000000000..87ce885a7
--- /dev/null
+++ b/infinigen/assets/table_decorations/utils.py
@@ -0,0 +1,324 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+@node_utils.to_nodegroup('nodegroup_star_profile', singleton=False, type='GeometryNodeTree')
+def nodegroup_star_profile(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Resolution', 64),
+ ('NodeSocketInt', 'Points', 64),
+ ('NodeSocketFloatDistance', 'Inner Radius', 0.9000)])
+
+ star = nw.new_node('GeometryNodeCurveStar',
+ input_kwargs={'Points': group_input.outputs["Points"], 'Inner Radius': group_input.outputs["Inner Radius"], 'Outer Radius': 1.0000})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve,
+ input_kwargs={'Curve': star.outputs["Curve"], 'Count': group_input.outputs["Resolution"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Curve': resample_curve}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_flip_index', singleton=False, type='GeometryNodeTree')
+def nodegroup_flip_index(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ index = nw.new_node(Nodes.Index)
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'V Resolution', 0),
+ ('NodeSocketInt', 'U Resolution', 0)])
+
+ modulo = nw.new_node(Nodes.Math,
+ input_kwargs={0: index, 1: group_input.outputs["V Resolution"]},
+ attrs={'operation': 'MODULO'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: modulo, 1: group_input.outputs["U Resolution"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: index, 1: group_input.outputs["V Resolution"]},
+ attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'FLOOR'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: floor})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Index': add}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_cylinder_side', singleton=False, type='GeometryNodeTree')
+def nodegroup_cylinder_side(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 0)])
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["V Resolution"], 1: 1.0000},
+ attrs={'operation': 'SUBTRACT'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': group_input.outputs["U Resolution"], 'Side Segments': subtract})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map', 3: cylinder.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': store_named_attribute, 'Top': cylinder.outputs["Top"], 'Side': cylinder.outputs["Side"], 'Bottom': cylinder.outputs["Bottom"]},
+ attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_shifted_circle', singleton=False, type='GeometryNodeTree')
+def nodegroup_shifted_circle(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Resolution', 32),
+ ('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'Z', 0.0000),
+ ('NodeSocketFloat', 'Rot Z', 0.0000)])
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle,
+ input_kwargs={'Resolution': group_input.outputs["Resolution"], 'Radius': group_input.outputs["Radius"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Z"]})
+
+ radians = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Rot Z"]}, attrs={'operation': 'RADIANS'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': radians})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_3.outputs["Curve"], 'Translation': combine_xyz, 'Rotation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_3}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_lofting', singleton=False, type='GeometryNodeTree')
+def nodegroup_lofting(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Profile Curves', None),
+ ('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 32),
+ ('NodeSocketBool', 'Use Nurb', False)])
+
+ cylinderside = nw.new_node(nodegroup_cylinder_side().name,
+ input_kwargs={'U Resolution': group_input.outputs["U Resolution"], 'V Resolution': group_input.outputs["V Resolution"]})
+
+ index = nw.new_node(Nodes.Index)
+
+ evaluate_on_domain = nw.new_node(Nodes.EvaluateonDomain, input_kwargs={1: index}, attrs={'data_type': 'INT', 'domain': 'CURVE'})
+
+ equal = nw.new_node(Nodes.Compare,
+ input_kwargs={2: evaluate_on_domain.outputs[1]},
+ attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+
+ curve_line = nw.new_node(Nodes.CurveLine)
+
+ domain_size = nw.new_node(Nodes.DomainSize, input_kwargs={'Geometry': group_input.outputs["Profile Curves"]}, attrs={'component': 'CURVE'})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line, 'Count': domain_size.outputs["Spline Count"]})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': group_input.outputs["Profile Curves"], 'Selection': equal, 'Instance': resample_curve})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ flipindex = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': domain_size.outputs["Spline Count"], 'U Resolution': group_input.outputs["U Resolution"]})
+
+ sample_index_2 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': group_input.outputs["Profile Curves"], 3: position, 'Index': flipindex},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': realize_instances, 'Position': sample_index_2.outputs[2]})
+
+ set_spline_type_1 = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position}, attrs={'spline_type': 'CATMULL_ROM'})
+
+ set_spline_type = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position}, attrs={'spline_type': 'NURBS'})
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["Use Nurb"], 14: set_spline_type_1, 15: set_spline_type})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': switch.outputs[6], 'Count': group_input.outputs["V Resolution"]})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ flipindex_1 = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': group_input.outputs["U Resolution"], 'U Resolution': group_input.outputs["V Resolution"]})
+
+ sample_index_3 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve_1, 3: position_1, 'Index': flipindex_1},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': cylinderside.outputs["Geometry"], 'Position': sample_index_3.outputs[2]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': set_position_1, 'Top': cylinderside.outputs["Top"], 'Side': cylinderside.outputs["Side"], 'Bottom': cylinderside.outputs["Bottom"]},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_lofting_poly', singleton=False, type='GeometryNodeTree')
+def nodegroup_lofting_poly(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Profile Curves', None),
+ ('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 32),
+ ('NodeSocketBool', 'Use Nurb', False)])
+
+ reroute_2 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["V Resolution"]})
+
+ cylinderside_001 = nw.new_node(nodegroup_cylinder_side().name,
+ input_kwargs={'U Resolution': group_input.outputs["U Resolution"], 'V Resolution': reroute_2})
+
+ index = nw.new_node(Nodes.Index)
+
+ evaluate_on_domain = nw.new_node(Nodes.EvaluateonDomain, input_kwargs={1: index}, attrs={'domain': 'CURVE', 'data_type': 'INT'})
+
+ equal = nw.new_node(Nodes.Compare,
+ input_kwargs={2: evaluate_on_domain.outputs[1]},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ curve_line = nw.new_node(Nodes.CurveLine)
+
+ domain_size = nw.new_node(Nodes.DomainSize,
+ input_kwargs={'Geometry': group_input.outputs["Profile Curves"]},
+ attrs={'component': 'CURVE'})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line, 'Count': domain_size.outputs["Spline Count"]})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': group_input.outputs["Profile Curves"], 'Selection': equal, 'Instance': resample_curve})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ flipindex_001 = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': domain_size.outputs["Spline Count"], 'U Resolution': group_input.outputs["U Resolution"]})
+
+ sample_index_2 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': group_input.outputs["Profile Curves"], 3: position, 'Index': flipindex_001},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': realize_instances, 'Position': sample_index_2.outputs[2]})
+
+ set_spline_type_1 = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position})
+
+ set_spline_type = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position}, attrs={'spline_type': 'NURBS'})
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["Use Nurb"], 14: set_spline_type_1, 15: set_spline_type})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': switch.outputs[6], 'Count': reroute_2})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ flipindex_001_1 = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': group_input.outputs["U Resolution"], 'U Resolution': reroute_2})
+
+ sample_index_3 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve_1, 3: position_1, 'Index': flipindex_001_1},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': cylinderside_001.outputs["Geometry"], 'Position': sample_index_3.outputs[2]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': set_position_1, 'Top': cylinderside_001.outputs["Top"], 'Side': cylinderside_001.outputs["Side"], 'Bottom': cylinderside_001.outputs["Bottom"]},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_warp_around_curve', singleton=False, type='GeometryNodeTree')
+def nodegroup_warp_around_curve(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketGeometry', 'Curve', None),
+ ('NodeSocketInt', 'Curve Resolution', 1024)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Curve Resolution"], 1: 1.0000})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': group_input.outputs["Curve"], 'Count': add})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ position_2 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_3 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_2})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Min"]})
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Max"]})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': separate_xyz_3.outputs["Z"], 1: separate_xyz_1.outputs["Z"], 2: separate_xyz_2.outputs["Z"]})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Curve Resolution"], 1: map_range.outputs["Result"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ round = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'ROUND'})
+
+ sample_index_3 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: position_1, 'Index': round},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ normal = nw.new_node(Nodes.InputNormal)
+
+ sample_index_5 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: normal, 'Index': round},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: sample_index_5.outputs[2], 'Scale': separate_xyz.outputs["X"]},
+ attrs={'operation': 'SCALE'})
+
+ curve_tangent = nw.new_node(Nodes.CurveTangent)
+
+ sample_index_4 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: curve_tangent, 'Index': round},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ cross_product = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: sample_index_4.outputs[2], 1: sample_index_5.outputs[2]},
+ attrs={'operation': 'CROSS_PRODUCT'})
+
+ scale_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: cross_product.outputs["Vector"], 'Scale': separate_xyz.outputs["Y"]},
+ attrs={'operation': 'SCALE'})
+
+ add_1 = nw.new_node(Nodes.VectorMath, input_kwargs={0: scale.outputs["Vector"], 1: scale_1.outputs["Vector"]})
+
+ add_2 = nw.new_node(Nodes.VectorMath, input_kwargs={0: sample_index_3.outputs[2], 1: add_1.outputs["Vector"]})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': group_input.outputs["Geometry"], 'Position': add_2.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_position}, attrs={'is_active_output': True})
diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py
new file mode 100644
index 000000000..437153ed5
--- /dev/null
+++ b/infinigen/assets/table_decorations/vase.py
@@ -0,0 +1,301 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint, choice, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+import infinigen.core.util.blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.table_decorations.utils import nodegroup_lofting, nodegroup_star_profile
+from infinigen.assets.material_assignments import AssetList
+
+class VaseFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(VaseFactory, self).__init__(factory_seed, coarse=coarse)
+
+ if dimensions is None:
+ z = uniform(0.17, 0.5)
+ x = z * uniform(0.3, 0.6)
+ dimensions = (x, x, z)
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['VaseFactory']()
+ params = {
+ 'Material': material_assignments['surface'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # all in meters
+ if dimensions is None:
+ z = uniform(0.25, 0.40)
+ x = uniform(0.2, 0.4) * z
+ dimensions = (x, x, z)
+
+ x, y, z = dimensions
+
+ U_resolution = 64
+ V_resolution = 64
+
+ neck_scale = uniform(0.2, 0.8)
+
+ parameters = {
+ 'Profile Inner Radius': choice([1.0, uniform(0.8, 1.0)]),
+ 'Profile Star Points': randint(16, U_resolution // 2 + 1),
+ 'U_resolution': U_resolution,
+ 'V_resolution': V_resolution,
+ 'Height': z,
+ 'Diameter': x,
+ 'Top Scale': neck_scale * uniform(0.8, 1.2),
+ 'Neck Mid Position': uniform(0.7, 0.95),
+ 'Neck Position': 0.5 * neck_scale + 0.5 + uniform(-0.05, 0.05),
+ 'Neck Scale': neck_scale,
+ 'Shoulder Position': uniform(0.3, 0.7),
+ 'Shoulder Thickness': uniform(0.1, 0.25),
+ 'Foot Scale': uniform(0.4, 0.6),
+ 'Foot Height': uniform(0.01, 0.1),
+ 'Material': choice(['glass', 'ceramic'])
+ }
+
+ return parameters
+
+ def create_asset(self, **params):
+ bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ surface.add_geomod(obj, geometry_vases, apply=True, input_kwargs=self.params)
+ butil.modify_mesh(obj, 'SOLIDIFY', apply=True, thickness=.002)
+ butil.modify_mesh(obj, 'SUBSURF', apply=True, levels=2, render_levels=2)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+@node_utils.to_nodegroup('nodegroup_vase_profile', singleton=False, type='GeometryNodeTree')
+def nodegroup_vase_profile(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Profile Curve', None),
+ ('NodeSocketFloat', 'Height', 0.0000), ('NodeSocketFloat', 'Diameter', 0.0000),
+ ('NodeSocketFloat', 'Top Scale', 0.0000), ('NodeSocketFloat', 'Neck Mid Position', 0.0000),
+ ('NodeSocketFloat', 'Neck Position', 0.5000), ('NodeSocketFloat', 'Neck Scale', 0.0000),
+ ('NodeSocketFloat', 'Shoulder Position', 0.0000), ('NodeSocketFloat', 'Shoulder Thickness', 0.0000),
+ ('NodeSocketFloat', 'Foot Scale', 0.0000), ('NodeSocketFloat', 'Foot Height', 0.0000)])
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Height"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Top Scale"],
+ 1: group_input.outputs["Diameter"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ neck_top = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 'Translation': combine_xyz_1,
+ 'Scale': multiply
+ })
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Height"],
+ 1: group_input.outputs["Neck Position"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Diameter"],
+ 1: group_input.outputs["Neck Scale"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ neck = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 'Translation': combine_xyz,
+ 'Scale': multiply_2
+ })
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: 1.0000, 1: group_input.outputs["Neck Position"]},
+ attrs={'use_clamp': True, 'operation': 'SUBTRACT'})
+
+ multiply_add = nw.new_node(Nodes.Math, input_kwargs={
+ 0: subtract,
+ 1: group_input.outputs["Neck Mid Position"],
+ 2: group_input.outputs["Neck Position"]
+ }, attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: group_input.outputs["Height"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_3})
+
+ add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Neck Scale"], 1: group_input.outputs["Top Scale"]})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: 2.0000}, attrs={'operation': 'DIVIDE'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Diameter"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ neck_middle = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 'Translation': combine_xyz_2,
+ 'Scale': multiply_4
+ })
+
+ neck_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [neck, neck_middle, neck_top]})
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={
+ 'Value': group_input.outputs["Shoulder Position"],
+ 3: group_input.outputs["Foot Height"],
+ 4: group_input.outputs["Neck Position"]
+ })
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Neck Position"],
+ 1: group_input.outputs["Foot Height"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ multiply_5 = nw.new_node(Nodes.Math,
+ input_kwargs={0: subtract_1, 1: group_input.outputs["Shoulder Thickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: map_range.outputs["Result"], 1: multiply_5})
+
+ minimum = nw.new_node(Nodes.Math, input_kwargs={0: add_1, 1: group_input.outputs["Neck Position"]},
+ attrs={'operation': 'MINIMUM'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: minimum, 1: group_input.outputs["Height"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_6})
+
+ body_top = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 'Translation': combine_xyz_3,
+ 'Scale': group_input.outputs["Diameter"]
+ })
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: map_range.outputs["Result"], 1: multiply_5},
+ attrs={'operation': 'SUBTRACT'})
+
+ maximum = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: group_input.outputs["Foot Height"]},
+ attrs={'operation': 'MAXIMUM'})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: maximum, 1: group_input.outputs["Height"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_7})
+
+ body_bottom = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input.outputs["Profile Curve"],
+ 'Translation': combine_xyz_5,
+ 'Scale': group_input.outputs["Diameter"]
+ })
+
+ body_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [body_bottom, body_top]})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Foot Height"],
+ 1: group_input.outputs["Height"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_8})
+
+ multiply_9 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Diameter"],
+ 1: group_input.outputs["Foot Scale"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ foot_top = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': group_input,
+ 'Translation': combine_xyz_4,
+ 'Scale': multiply_9
+ })
+
+ foot_bottom = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': group_input, 'Scale': multiply_9})
+
+ foot_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [foot_bottom, foot_top]})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [foot_geometry, body_geometry, neck_geometry]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_2},
+ attrs={'is_active_output': True})
+
+
+def geometry_vases(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+ starprofile = nw.new_node(nodegroup_star_profile().name, input_kwargs={
+ 'Resolution': kwargs['U_resolution'],
+ 'Points': kwargs['Profile Star Points'],
+ 'Inner Radius': kwargs['Profile Inner Radius']
+ })
+
+ vaseprofile = nw.new_node(nodegroup_vase_profile().name, input_kwargs={
+ 'Profile Curve': starprofile.outputs["Curve"],
+ 'Height': kwargs['Height'],
+ 'Diameter': kwargs['Diameter'],
+ 'Top Scale': kwargs['Top Scale'],
+ 'Neck Mid Position': kwargs['Neck Mid Position'],
+ 'Neck Position': kwargs['Neck Position'],
+ 'Neck Scale': kwargs['Neck Scale'],
+ 'Shoulder Position': kwargs['Shoulder Position'],
+ 'Shoulder Thickness': kwargs['Shoulder Thickness'],
+ 'Foot Scale': kwargs['Foot Scale'],
+ 'Foot Height': kwargs['Foot Height']
+ })
+
+ lofting = nw.new_node(nodegroup_lofting().name,
+ input_kwargs={'Profile Curves': vaseprofile, 'U Resolution': 64, 'V Resolution': 64})
+
+ delete_geometry = nw.new_node(Nodes.DeleteGeometry, input_kwargs={
+ 'Geometry': lofting.outputs["Geometry"],
+ 'Selection': lofting.outputs["Top"]
+ })
+
+ set_material = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': delete_geometry, 'Material': kwargs['Material']})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material},
+ attrs={'is_active_output': True})
+
+
diff --git a/infinigen/assets/tables/__init__.py b/infinigen/assets/tables/__init__.py
new file mode 100644
index 000000000..96d9cfa0b
--- /dev/null
+++ b/infinigen/assets/tables/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .cocktail_table import TableCocktailFactory
+from .dining_table import TableDiningFactory, SideTableFactory, CoffeeTableFactory
+from .table_top import TableTopFactory
diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py
new file mode 100644
index 000000000..ec7e30f7d
--- /dev/null
+++ b/infinigen/assets/tables/cocktail_table.py
@@ -0,0 +1,269 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Yiming Zuo: primary author
+# - Alexander Raistrick: implement placeholder
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint, choice
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.surface import NoApply
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core import tagging, tags as t
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.tables.table_utils import nodegroup_create_anchors, nodegroup_create_legs_and_strechers
+from infinigen.assets.tables.table_top import nodegroup_generate_table_top
+
+from infinigen.assets.tables.legs.single_stand import nodegroup_generate_single_stand
+from infinigen.assets.tables.legs.straight import nodegroup_generate_leg_straight
+from infinigen.assets.tables.legs.wheeled import nodegroup_wheeled_leg
+
+from infinigen.assets.tables.strechers import nodegroup_strecher
+
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+@node_utils.to_nodegroup('geometry_create_legs', singleton=False, type='GeometryNodeTree')
+def geometry_create_legs(nw: NodeWrangler, **kwargs):
+
+ createanchors = nw.new_node(nodegroup_create_anchors().name,
+ input_kwargs={'Profile N-gon': kwargs['Leg Number'], 'Profile Width': kwargs['Leg Placement Top Relative Scale']*kwargs['Top Profile Width'], 'Profile Aspect Ratio': 1.0000})
+
+ if kwargs['Leg Style'] == "single_stand":
+ leg = nw.new_node(nodegroup_generate_single_stand(**kwargs).name,
+ input_kwargs={'Leg Height': kwargs['Leg Height'],
+ 'Leg Diameter': kwargs['Leg Diameter'],
+ 'Resolution': 64})
+
+ leg = nw.new_node(nodegroup_create_legs_and_strechers().name,
+ input_kwargs={'Anchors': createanchors,
+ 'Keep Legs': True,
+ 'Leg Instance': leg,
+ 'Table Height': kwargs['Top Height'],
+ 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'],
+ 'Align Leg X rot': True
+ })
+
+ elif kwargs['Leg Style'] == "straight":
+ leg = nw.new_node(nodegroup_generate_leg_straight(**kwargs).name,
+ input_kwargs={'Leg Height': kwargs['Leg Height'],
+ 'Leg Diameter': kwargs['Leg Diameter'],
+ 'Resolution': 32,
+ 'N-gon': kwargs['Leg NGon'],
+ 'Fillet Ratio': 0.1})
+
+ strecher = nw.new_node(nodegroup_strecher().name,
+ input_kwargs={'Profile Width': kwargs['Leg Diameter'] * 0.5})
+
+ leg = nw.new_node(nodegroup_create_legs_and_strechers().name,
+ input_kwargs={
+ 'Anchors': createanchors,
+ 'Keep Legs': True,
+ 'Leg Instance': leg,
+ 'Table Height': kwargs['Top Height'],
+ 'Strecher Instance': strecher,
+ 'Strecher Index Increment': kwargs['Strecher Increament'],
+ 'Strecher Relative Position': kwargs['Strecher Relative Pos'],
+ 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'],
+ 'Align Leg X rot': True
+ })
+
+ elif kwargs['Leg Style'] == "wheeled":
+ leg = nw.new_node(nodegroup_wheeled_leg(**kwargs).name,
+ input_kwargs={
+ 'Joint Height': kwargs['Leg Joint Height'],
+ 'Leg Diameter': kwargs['Leg Diameter'],
+ 'Top Height': kwargs['Top Height'],
+ 'Wheel Width': kwargs['Leg Wheel Width'],
+ 'Wheel Rotation': kwargs['Leg Wheel Rot'],
+ 'Pole Length': kwargs['Leg Pole Length'],
+ 'Leg Number': kwargs['Leg Pole Number'],
+ })
+
+ else:
+ raise NotImplementedError
+
+ leg = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': leg, 'Material': kwargs['LegMaterial']})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': leg}, attrs={'is_active_output': True})
+
+def geometry_assemble_table(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ generatetabletop = nw.new_node(nodegroup_generate_table_top().name,
+ input_kwargs={
+ 'Thickness': kwargs['Top Thickness'],
+ 'N-gon': kwargs['Top Profile N-gon'],
+ 'Profile Width': kwargs['Top Profile Width'],
+ 'Aspect Ratio': kwargs['Top Profile Aspect Ratio'],
+ 'Fillet Ratio': kwargs['Top Profile Fillet Ratio'],
+ 'Fillet Radius Vertical': kwargs['Top Vertical Fillet Ratio'],
+ })
+
+ tabletop_instance = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': generatetabletop,
+ 'Translation': (0.0000, 0.0000, kwargs['Top Height'])})
+
+ tabletop_instance = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': tabletop_instance, 'Material': kwargs['TopMaterial']})
+
+ legs = nw.new_node(geometry_create_legs(**kwargs).name)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [tabletop_instance, legs]})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': generatetabletop.outputs["Curve"]})
+ fill_curve = nw.new_node(Nodes.FillCurve, input_kwargs={'Curve': resample_curve})
+
+ voff = kwargs['Top Height'] + kwargs['Top Thickness']
+ extrude_mesh = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={'Mesh': fill_curve, 'Offset Scale': -voff, 'Individual': False})
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [extrude_mesh.outputs["Mesh"], fill_curve]})
+ transform_geometry_1 = nw.new_node(
+ Nodes.Transform, input_kwargs={
+ 'Geometry': join_geometry_1, 'Translation': (0, 0, voff)
+ }
+ )
+ switch = nw.new_node(Nodes.Switch, input_kwargs={1: kwargs['is_placeholder'], 14: join_geometry, 15: transform_geometry_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': switch}, attrs={'is_active_output': True})
+
+
+class TableCocktailFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(TableCocktailFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ from infinigen.assets.clothes import blanket
+ from infinigen.assets.scatters.clothes import ClothesCover
+ # self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(.8, 1.2),
+ # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply()
+ self.clothes_scatter = NoApply()
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['TableCocktailFactory']()
+ params = {
+ "TopMaterial": material_assignments['top'].assign_material(),
+ "LegMaterial": material_assignments['leg'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # all in meters
+ if dimensions is None:
+ x = uniform(0.5, 0.8)
+ z = uniform(1.0, 1.5)
+ dimensions = (
+ x, x, z
+ )
+
+ x, y, z = dimensions
+
+ NGon = choice([4, 32])
+ if NGon >= 32:
+ round_table = True
+ else:
+ round_table = False
+
+ leg_style = choice(['straight', 'single_stand'])
+ if leg_style == "single_stand":
+ leg_number = 1
+ leg_diameter = uniform(0.7*x, 0.9*x)
+
+ leg_curve_ctrl_pts = [(0.0, uniform(0.1, 0.2)),
+ (0.5, uniform(0.1, 0.2)), (0.9, uniform(0.2, 0.3)), (1.0, 1.0)]
+
+ elif leg_style == "straight":
+ leg_diameter = uniform(0.05, 0.07)
+
+ if round_table:
+ leg_number = choice([3, 4])
+ else:
+ leg_number = NGon
+
+ leg_curve_ctrl_pts = [(0.0, 1.0), (0.4, uniform(0.85, 0.95)), (1.0, uniform(0.4, 0.6))]
+
+ else:
+ raise NotImplementedError
+
+ top_thickness = uniform(0.02, 0.05)
+
+ parameters = {
+ 'Top Profile N-gon': 32 if round_table else 4,
+ 'Top Profile Width': x if round_table else 1.414 * x,
+ 'Top Profile Aspect Ratio': 1.0,
+ 'Top Profile Fillet Ratio': 0.499 if round_table else uniform(0.0, 0.05),
+ 'Top Thickness': top_thickness,
+ 'Top Vertical Fillet Ratio': uniform(0.1, 0.3),
+ # 'Top Material': choice(['marble', 'tiled_wood', 'plastic', 'glass']),
+ 'Height': z,
+ 'Top Height': z - top_thickness,
+ 'Leg Number': leg_number,
+ 'Leg Style': leg_style,
+ 'Leg NGon': choice([4, 32]),
+ 'Leg Placement Top Relative Scale': 0.7,
+ 'Leg Placement Bottom Relative Scale': uniform(1.1, 1.3),
+ 'Leg Height': 1.0,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood', 'glass']),
+ 'Strecher Relative Pos': uniform(0.2, 0.6),
+ 'Strecher Increament': choice([0, 1, 2])
+ }
+
+ return parameters
+
+ def _execute_geonodes(self, is_placeholder):
+
+ bpy.ops.mesh.primitive_plane_add(
+ size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ kwargs = {**self.params, 'is_placeholder': is_placeholder}
+ surface.add_geomod(obj, geometry_assemble_table, apply=True, input_kwargs=kwargs)
+ tagging.tag_system.relabel_obj(obj)
+
+ return obj
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return self._execute_geonodes(is_placeholder=True)
+
+ def create_asset(self, **_):
+ return self._execute_geonodes(is_placeholder=False)
+
+ def finalize_assets(self, assets):
+ self.clothes_scatter.apply(assets)
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py
new file mode 100644
index 000000000..9c956b499
--- /dev/null
+++ b/infinigen/assets/tables/dining_table.py
@@ -0,0 +1,312 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+from collections.abc import Iterable
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint, choice
+
+# from infinigen.assets.materials import metal, metal_shader_list
+# from infinigen.assets.materials.leather_and_fabrics import fabric
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.surface import NoApply
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core import tagging, tags as t
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.tables.table_utils import nodegroup_create_anchors, nodegroup_create_legs_and_strechers
+from infinigen.assets.tables.table_top import nodegroup_generate_table_top
+
+from infinigen.assets.tables.legs.single_stand import nodegroup_generate_single_stand
+from infinigen.assets.tables.legs.straight import nodegroup_generate_leg_straight
+from infinigen.assets.tables.legs.square import nodegroup_generate_leg_square
+
+from infinigen.assets.tables.strechers import nodegroup_strecher
+
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+
+@node_utils.to_nodegroup('geometry_create_legs', singleton=False, type='GeometryNodeTree')
+def geometry_create_legs(nw: NodeWrangler, **kwargs):
+ createanchors = nw.new_node(nodegroup_create_anchors().name, input_kwargs={
+ 'Profile N-gon': kwargs['Leg Number'],
+ 'Profile Width': kwargs['Leg Placement Top Relative Scale'] * kwargs['Top Profile Width'],
+ 'Profile Aspect Ratio': kwargs['Top Profile Aspect Ratio']
+ })
+
+ if kwargs['Leg Style'] == "single_stand":
+ leg = nw.new_node(nodegroup_generate_single_stand(**kwargs).name, input_kwargs={
+ 'Leg Height': kwargs['Leg Height'],
+ 'Leg Diameter': kwargs['Leg Diameter'],
+ 'Resolution': 64
+ })
+
+ leg = nw.new_node(nodegroup_create_legs_and_strechers().name, input_kwargs={
+ 'Anchors': createanchors,
+ 'Keep Legs': True,
+ 'Leg Instance': leg,
+ 'Table Height': kwargs['Top Height'],
+ 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'],
+ 'Align Leg X rot': True
+ })
+
+ elif kwargs['Leg Style'] == "straight":
+ leg = nw.new_node(nodegroup_generate_leg_straight(**kwargs).name, input_kwargs={
+ 'Leg Height': kwargs['Leg Height'],
+ 'Leg Diameter': kwargs['Leg Diameter'],
+ 'Resolution': 32,
+ 'N-gon': kwargs['Leg NGon'],
+ 'Fillet Ratio': 0.1
+ })
+
+ strecher = nw.new_node(nodegroup_strecher().name,
+ input_kwargs={'Profile Width': kwargs['Leg Diameter'] * 0.5})
+
+ leg = nw.new_node(nodegroup_create_legs_and_strechers().name, input_kwargs={
+ 'Anchors': createanchors,
+ 'Keep Legs': True,
+ 'Leg Instance': leg,
+ 'Table Height': kwargs['Top Height'],
+ 'Strecher Instance': strecher,
+ 'Strecher Index Increment': kwargs['Strecher Increament'],
+ 'Strecher Relative Position': kwargs['Strecher Relative Pos'],
+ 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'],
+ 'Align Leg X rot': True
+ })
+
+ elif kwargs['Leg Style'] == "square":
+ leg = nw.new_node(nodegroup_generate_leg_square(**kwargs).name, input_kwargs={
+ 'Height': kwargs['Leg Height'],
+ 'Width': 0.707 * kwargs['Leg Placement Top Relative Scale'] * kwargs['Top Profile Width'] * kwargs[
+ 'Top Profile Aspect Ratio'],
+ 'Has Bottom Connector': (kwargs['Strecher Increament'] > 0),
+ 'Profile Width': kwargs['Leg Diameter']
+ })
+
+ leg = nw.new_node(nodegroup_create_legs_and_strechers().name, input_kwargs={
+ 'Anchors': createanchors,
+ 'Keep Legs': True,
+ 'Leg Instance': leg,
+ 'Table Height': kwargs['Top Height'],
+ 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'],
+ 'Align Leg X rot': True
+ })
+
+ else:
+ raise NotImplementedError
+
+ leg = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': leg, 'Material': kwargs['LegMaterial']})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': leg},
+ attrs={'is_active_output': True})
+
+
+def geometry_assemble_table(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ generatetabletop = nw.new_node(nodegroup_generate_table_top().name, input_kwargs={
+ 'Thickness': kwargs['Top Thickness'],
+ 'N-gon': kwargs['Top Profile N-gon'],
+ 'Profile Width': kwargs['Top Profile Width'],
+ 'Aspect Ratio': kwargs['Top Profile Aspect Ratio'],
+ 'Fillet Ratio': kwargs['Top Profile Fillet Ratio'],
+ 'Fillet Radius Vertical': kwargs['Top Vertical Fillet Ratio']
+ })
+
+ tabletop_instance = nw.new_node(Nodes.Transform, input_kwargs={
+ 'Geometry': generatetabletop,
+ 'Translation': (0.0000, 0.0000, kwargs['Top Height'])
+ })
+
+ tabletop_instance = nw.new_node(Nodes.SetMaterial,
+ input_kwargs={'Geometry': tabletop_instance, 'Material': kwargs['TopMaterial']})
+
+ legs = nw.new_node(geometry_create_legs(**kwargs).name)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [tabletop_instance, legs]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry},
+ attrs={'is_active_output': True})
+
+
+class TableDiningFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(TableDiningFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ from infinigen.assets.clothes import blanket
+
+ from infinigen.assets.scatters.clothes import ClothesCover
+ # self.clothes_scatter = ClothesCover(factory_fn=blanket.BlanketFactory, width=log_uniform(.8, 1.2),
+ # size=uniform(.8, 1.2)) if uniform() < .3 else NoApply()
+ self.clothes_scatter = NoApply()
+ self.material_params, self.scratch, self.edge_wear = self.get_material_params()
+
+ self.params.update(self.material_params)
+
+ def get_material_params(self):
+ material_assignments = AssetList['TableDiningFactory']()
+ params = {
+ "TopMaterial": material_assignments['top'].assign_material(),
+ "LegMaterial": material_assignments['leg'].assign_material(),
+ }
+ wrapped_params = {
+ k: surface.shaderfunc_to_material(v) for k, v in params.items()
+ }
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = uniform() < scratch_prob
+ is_edge_wear = uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return wrapped_params, scratch, edge_wear
+
+
+ @staticmethod
+ def sample_parameters(dimensions):
+
+ if dimensions is None:
+
+ width = uniform(0.91, 1.16)
+
+ if uniform() < 0.7:
+ # oblong
+ length = uniform(1.4, 2.8)
+ else:
+ # approx square
+ length = width * normal(1, 0.1)
+
+ dimensions = (
+ length,
+ width,
+ uniform(0.65, 0.85)
+ )
+
+ # all in meters
+ x, y, z = dimensions
+
+ NGon = 4
+
+ leg_style = choice(['straight', 'single_stand', 'square'], p=[0.5, 0.1, 0.4])
+ # leg_style = choice(['straight'])
+
+ if leg_style == "single_stand":
+ leg_number = 2
+ leg_diameter = uniform(0.22 * x, 0.28 * x)
+
+ leg_curve_ctrl_pts = [(0.0, uniform(0.1, 0.2)), (0.5, uniform(0.1, 0.2)), (0.9, uniform(0.2, 0.3)),
+ (1.0, 1.0)]
+
+ top_scale = uniform(0.6, 0.7)
+ bottom_scale = 1.0
+
+ elif leg_style == "square":
+ leg_number = 2
+ leg_diameter = uniform(0.07, 0.10)
+
+ leg_curve_ctrl_pts = None
+
+ top_scale = 0.8
+ bottom_scale = 1.0
+
+ elif leg_style == "straight":
+ leg_diameter = uniform(0.05, 0.07)
+
+ leg_number = 4
+
+ leg_curve_ctrl_pts = [(0.0, 1.0), (0.4, uniform(0.85, 0.95)), (1.0, uniform(0.4, 0.6))]
+
+ top_scale = 0.8
+ bottom_scale = uniform(1.0, 1.2)
+
+ else:
+ raise NotImplementedError
+
+ top_thickness = uniform(0.03, 0.06)
+
+ parameters = {
+ 'Top Profile N-gon': NGon,
+ 'Top Profile Width': 1.414 * x,
+ 'Top Profile Aspect Ratio': y / x,
+ 'Top Profile Fillet Ratio': uniform(0.0, 0.02),
+ 'Top Thickness': top_thickness,
+ 'Top Vertical Fillet Ratio': uniform(0.1, 0.3),
+ # 'Top Material': choice(['marble', 'tiled_wood', 'metal', 'fabric'], p=[.3, .3, .2, .2]),
+ 'Height': z,
+ 'Top Height': z - top_thickness,
+ 'Leg Number': leg_number,
+ 'Leg Style': leg_style,
+ 'Leg NGon': 4,
+ 'Leg Placement Top Relative Scale': top_scale,
+ 'Leg Placement Bottom Relative Scale': bottom_scale,
+ 'Leg Height': 1.0,
+ 'Leg Diameter': leg_diameter,
+ 'Leg Curve Control Points': leg_curve_ctrl_pts,
+ # 'Leg Material': choice(['metal', 'wood', 'glass', 'plastic']),
+ 'Strecher Relative Pos': uniform(0.2, 0.6),
+ 'Strecher Increament': choice([0, 1, 2])
+ }
+
+ return parameters
+
+ def create_asset(self, **params):
+ bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0),
+ scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ # surface.add_geomod(obj, geometry_assemble_table, apply=False, input_kwargs=self.params)
+ surface.add_geomod(obj, geometry_assemble_table, apply=True, input_kwargs=self.params)
+ tagging.tag_system.relabel_obj(obj)
+ assert tagging.tagged_face_mask(obj, {t.Subpart.SupportSurface}).sum() != 0
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+ #def finalize_assets(self, assets):
+ # self.clothes_scatter.apply(assets)
+
+class SideTableFactory(TableDiningFactory):
+
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ if dimensions is None:
+ w = 0.55 * normal(1, 0.05)
+ h = 0.95 * w * normal(1, 0.05)
+ dimensions = (w, w, h)
+ super().__init__(factory_seed, coarse=coarse, dimensions=dimensions)
+
+class CoffeeTableFactory(TableDiningFactory):
+
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ if dimensions is None:
+ dimensions = (
+ uniform(1, 1.5),
+ uniform(0.6, 0.9),
+ uniform(0.4, 0.5)
+ )
+ super().__init__(factory_seed, coarse=coarse, dimensions=dimensions)
+
diff --git a/infinigen/assets/tables/legs/single_stand.py b/infinigen/assets/tables/legs/single_stand.py
new file mode 100644
index 000000000..1425e9190
--- /dev/null
+++ b/infinigen/assets/tables/legs/single_stand.py
@@ -0,0 +1,34 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_cylinder, nodegroup_generate_radius_curve
+
+@node_utils.to_nodegroup('nodegroup_generate_single_stand', singleton=False, type='GeometryNodeTree')
+def nodegroup_generate_single_stand(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Leg Height', 0.0000),
+ ('NodeSocketFloat', 'Leg Diameter', 1.0000),
+ ('NodeSocketInt', 'Resolution', 64)])
+
+ generateradiuscurve = nw.new_node(nodegroup_generate_radius_curve(kwargs['Leg Curve Control Points']).name, input_kwargs={'Resolution': group_input.outputs["Resolution"]})
+
+ ngoncylinder = nw.new_node(nodegroup_n_gon_cylinder().name,
+ input_kwargs={'Radius Curve': generateradiuscurve, 'Height': group_input.outputs["Leg Height"], 'N-gon': group_input.outputs["Resolution"], 'Profile Width': group_input.outputs["Leg Diameter"], 'Aspect Ratio': 1.0000, 'Fillet Ratio': 0.0000, 'Resolution': group_input.outputs["Resolution"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': ngoncylinder.outputs["Mesh"]},
+ attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tables/legs/square.py b/infinigen/assets/tables/legs/square.py
new file mode 100644
index 000000000..7d99d728e
--- /dev/null
+++ b/infinigen/assets/tables/legs/square.py
@@ -0,0 +1,74 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_profile, nodegroup_merge_curve
+
+@node_utils.to_nodegroup('nodegroup_generate_leg_square', singleton=False, type='GeometryNodeTree')
+def nodegroup_generate_leg_square(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[
+ ('NodeSocketFloat', 'Width', 0.0000),
+ ('NodeSocketFloat', 'Height', 0.0000),
+ ('NodeSocketFloatDistance', 'Fillet Radius', 0.0300),
+ ('NodeSocketBool', 'Has Bottom Connector', True),
+ ('NodeSocketInt', 'Profile N-gon', 4),
+ ('NodeSocketFloatDistance', 'Profile Width', 0.1000),
+ ('NodeSocketFloatDistance', 'Profile Aspect Ratio', 0.5000),
+ ('NodeSocketFloat', 'Profile Fillet Ratio', 0.1000)])
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Has Bottom Connector"], 1: 4.0000})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': group_input.outputs["Has Bottom Connector"], 3: 4.7124, 4: 6.2832})
+
+ arc = nw.new_node('GeometryNodeCurveArc',
+ input_kwargs={'Resolution': add, 'Radius': 0.7071, 'Sweep Angle': map_range.outputs["Result"]})
+
+ mergecurve = nw.new_node(nodegroup_merge_curve().name, input_kwargs={'Curve': arc.outputs["Curve"]})
+
+ map_range_1 = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': group_input.outputs["Has Bottom Connector"], 3: 1.5708, 4: 3.1416})
+
+ set_curve_tilt = nw.new_node(Nodes.SetCurveTilt, input_kwargs={'Curve': mergecurve, 'Tilt': map_range_1.outputs["Result"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': set_curve_tilt, 'Rotation': (0.0000, 0.0000, -0.7854)})
+
+ transform_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform, 'Translation': (0.0000, 0.0000, -0.5000), 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Width"], 'Y': 1.0000, 'Z': group_input.outputs["Height"]})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_1, 'Scale': combine_xyz})
+
+ set_curve_radius = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': transform_2, 'Radius': 1.0000})
+
+ fillet_curve = nw.new_node(Nodes.FilletCurve,
+ input_kwargs={'Curve': set_curve_radius, 'Count': 8, 'Radius': group_input.outputs["Fillet Radius"], 'Limit Radius': True},
+ attrs={'mode': 'POLY'})
+
+ ngonprofile = nw.new_node(nodegroup_n_gon_profile().name,
+ input_kwargs={'Profile N-gon': group_input.outputs["Profile N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Profile Aspect Ratio': group_input.outputs["Profile Aspect Ratio"], 'Profile Fillet Ratio': group_input.outputs["Profile Fillet Ratio"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': fillet_curve, 'Profile Curve': ngonprofile, 'Fill Caps': True})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh, 'Rotation': (0.0000, 0.0000, 1.5708)})
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': transform_3, 'Shade Smooth': False})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tables/legs/straight.py b/infinigen/assets/tables/legs/straight.py
new file mode 100644
index 000000000..0f619d1e5
--- /dev/null
+++ b/infinigen/assets/tables/legs/straight.py
@@ -0,0 +1,36 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_cylinder, nodegroup_generate_radius_curve
+
+@node_utils.to_nodegroup('nodegroup_generate_leg_straight', singleton=False, type='GeometryNodeTree')
+def nodegroup_generate_leg_straight(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Leg Height', 0.0000),
+ ('NodeSocketFloat', 'Leg Diameter', 1.0000),
+ ('NodeSocketInt', 'Resolution', 0),
+ ('NodeSocketInt', 'N-gon', 32),
+ ('NodeSocketFloat', 'Fillet Ratio', 0.0100)])
+
+ generateradiuscurve = nw.new_node(nodegroup_generate_radius_curve(kwargs['Leg Curve Control Points']).name, input_kwargs={'Resolution': group_input.outputs["Resolution"]})
+
+ ngoncylinder = nw.new_node(nodegroup_n_gon_cylinder().name,
+ input_kwargs={'Radius Curve': generateradiuscurve, 'Height': group_input.outputs["Leg Height"], 'N-gon': group_input.outputs["N-gon"], 'Profile Width': group_input.outputs["Leg Diameter"], 'Aspect Ratio': 1.0000, 'Fillet Ratio': group_input.outputs["Fillet Ratio"], 'Resolution': group_input.outputs["Resolution"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': ngoncylinder.outputs["Mesh"]},
+ attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tables/legs/wheeled.py b/infinigen/assets/tables/legs/wheeled.py
new file mode 100644
index 000000000..874914234
--- /dev/null
+++ b/infinigen/assets/tables/legs/wheeled.py
@@ -0,0 +1,217 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_top import nodegroup_capped_cylinder
+from infinigen.assets.tables.table_utils import nodegroup_arc_top, nodegroup_n_gon_cylinder, nodegroup_align_bottom_to_floor, nodegroup_create_anchors, nodegroup_create_legs_and_strechers
+
+@node_utils.to_nodegroup('nodegroup_chair_wheel', singleton=False, type='GeometryNodeTree')
+def nodegroup_chair_wheel(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Arc Sweep Angle', 240.0000),
+ ('NodeSocketFloat', 'Wheel Width', 0.0000),
+ ('NodeSocketFloat', 'Wheel Rotation', 0.5000),
+ ('NodeSocketFloat', 'Pole Width', 0.0000),
+ ('NodeSocketFloat', 'Pole Aspect Ratio', 0.6000),
+ ('NodeSocketFloat', 'Pole Length', 3.0000)])
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': group_input.outputs["Wheel Width"]})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Wheel Width"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_1, 'End': combine_xyz_2})
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 0.0200
+
+ value_1 = nw.new_node(Nodes.Value)
+ value_1.outputs[0].default_value = 0.5000
+
+ cappedcylinder = nw.new_node(nodegroup_capped_cylinder().name,
+ input_kwargs={'Thickness': value, 'Radius': value_1, 'Cap Relative Scale': 0.0100})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: value, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': cappedcylinder, 'Translation': combine_xyz, 'Rotation': (-1.5708, 0.0000, 0.0000)})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ align_euler_to_vector = nw.new_node(Nodes.AlignEulerToVector, input_kwargs={'Vector': position}, attrs={'axis': 'Y'})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': curve_line, 'Instance': transform, 'Rotation': align_euler_to_vector})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: value_1, 1: 0.0800})
+
+ arctop = nw.new_node(nodegroup_arc_top().name,
+ input_kwargs={'Diameter': add, 'Sweep Angle': group_input.outputs["Arc Sweep Angle"]})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Wheel Width"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', input_kwargs={'Width': multiply_2, 'Height': 0.0200})
+
+ fillet_curve = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': quadrilateral, 'Count': 4, 'Radius': 0.0300, 'Limit Radius': True},
+ attrs={'mode': 'POLY'})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': arctop, 'Profile Curve': fillet_curve, 'Fill Caps': True})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: value_1, 1: 0.1000}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: value_1, 1: 0.4000}, attrs={'operation': 'MULTIPLY'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Side Segments': 8, 'Fill Segments': 4, 'Radius': multiply_3, 'Depth': multiply_4})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: value_1, 1: 0.4400}, attrs={'operation': 'MULTIPLY'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: value_1, 1: 0.4500}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Z': multiply_6})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [instance_on_points, curve_to_mesh, transform_2]})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_5, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_7})
+
+ transform_6 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_4})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Pole Length"], 1: 0.1500},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Pole Width"], 1: -0.3535, 2: -0.3000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': subtract, 'Z': multiply_add})
+
+ radians = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Wheel Rotation"]}, attrs={'operation': 'RADIANS'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': radians})
+
+ transform_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_6, 'Translation': combine_xyz_5, 'Rotation': combine_xyz_6})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (1.0000, 0.0000, -1.0000), 'End': (1.0000, 0.0000, 1.0000)})
+
+ ngoncylinder = nw.new_node(nodegroup_n_gon_cylinder().name,
+ input_kwargs={'Radius Curve': curve_line_1, 'Height': group_input.outputs["Pole Length"], 'N-gon': 4, 'Profile Width': group_input.outputs["Pole Width"], 'Aspect Ratio': group_input.outputs["Pole Aspect Ratio"], 'Fillet Ratio': 0.1500, 'Resolution': 32})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': ngoncylinder.outputs["Mesh"], 'Rotation': (0.0000, -1.5708, 0.0000)})
+
+ subdivision_surface_1 = nw.new_node(Nodes.SubdivisionSurface, input_kwargs={'Mesh': transform_3, 'Level': 0})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_4, subdivision_surface_1]})
+
+ value_2 = nw.new_node(Nodes.Value)
+ value_2.outputs[0].default_value = 0.1500
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry_1, 'Scale': value_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_wheeled_leg', singleton=False, type='GeometryNodeTree')
+def nodegroup_wheeled_leg(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Joint Height', 0.0000),
+ ('NodeSocketFloat', 'Leg Diameter', 0.0000),
+ ('NodeSocketFloat', 'Top Height', 0.0000),
+ ('NodeSocketFloat', 'Arc Sweep Angle', 240.0000),
+ ('NodeSocketFloat', 'Wheel Width', 0.1300),
+ ('NodeSocketFloat', 'Wheel Rotation', 0.5000),
+ ('NodeSocketFloat', 'Pole Length', 1.8000),
+ ('NodeSocketInt', 'Leg Number', 5)])
+
+ value_1 = nw.new_node(Nodes.Value)
+ value_1.outputs[0].default_value = 0.0010
+
+ createanchors = nw.new_node(nodegroup_create_anchors().name,
+ input_kwargs={'Profile N-gon': group_input.outputs["Leg Number"], 'Profile Width': value_1, 'Profile Aspect Ratio': 1.0000})
+
+ chair_wheel = nw.new_node(nodegroup_chair_wheel().name,
+ input_kwargs={'Arc Sweep Angle': group_input.outputs["Arc Sweep Angle"], 'Wheel Width': group_input.outputs["Wheel Width"], 'Wheel Rotation': group_input.outputs["Wheel Rotation"], 'Pole Width': 0.5000, 'Pole Length': group_input.outputs["Pole Length"]})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': chair_wheel, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: 2.0000, 1: value_1}, attrs={'operation': 'DIVIDE'})
+
+ createlegsandstrechers = nw.new_node(nodegroup_create_legs_and_strechers().name,
+ input_kwargs={'Anchors': createanchors, 'Keep Legs': True, 'Leg Instance': transform_geometry, 'Table Height': 0.0250, 'Leg Bottom Relative Scale': divide, 'Strecher Index Increment': 1, 'Strecher Relative Position': 1.0000, 'Leg Bottom Offset': 0.0250, 'Align Leg X rot': True})
+
+ alignbottomtofloor = nw.new_node(nodegroup_align_bottom_to_floor().name, input_kwargs={'Geometry': createlegsandstrechers})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Leg Diameter"]}, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Joint Height"], 1: alignbottomtofloor.outputs["Offset"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Vertices': 64, 'Radius': multiply, 'Depth': subtract})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: alignbottomtofloor.outputs["Offset"]})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_1})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: 0.0025}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Top Height"], 1: group_input.outputs["Joint Height"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ cylinder_1 = nw.new_node('GeometryNodeMeshCylinder', input_kwargs={'Vertices': 64, 'Radius': subtract_1, 'Depth': subtract_2})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={1: subtract_2}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_3 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Top Height"], 1: multiply_2},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': subtract_3})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cylinder_1.outputs["Mesh"], 'Translation': combine_xyz_2})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [alignbottomtofloor.outputs["Geometry"], transform_geometry_2, transform_geometry_3]})
+
+ # multiply_3 = nw.new_node(Nodes.Math,
+ # input_kwargs={0: group_input.outputs["Top Height"], 1: -1.0000},
+ # attrs={'operation': 'MULTIPLY'})
+
+ # combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_3})
+
+ # transform_geometry_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': join_geometry, 'Translation': combine_xyz_3})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tables/lofting.py b/infinigen/assets/tables/lofting.py
new file mode 100644
index 000000000..7156e4944
--- /dev/null
+++ b/infinigen/assets/tables/lofting.py
@@ -0,0 +1,298 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+
+
+@node_utils.to_nodegroup('nodegroup_flip_index', singleton=False, type='GeometryNodeTree')
+def nodegroup_flip_index(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ index = nw.new_node(Nodes.Index)
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'V Resolution', 0),
+ ('NodeSocketInt', 'U Resolution', 0)])
+
+ modulo = nw.new_node(Nodes.Math,
+ input_kwargs={0: index, 1: group_input.outputs["V Resolution"]},
+ attrs={'operation': 'MODULO'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: modulo, 1: group_input.outputs["U Resolution"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: index, 1: group_input.outputs["V Resolution"]},
+ attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'FLOOR'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: floor})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Index': add}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_cylinder_side', singleton=False, type='GeometryNodeTree')
+def nodegroup_cylinder_side(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 0)])
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["V Resolution"], 1: 1.0000},
+ attrs={'operation': 'SUBTRACT'})
+
+ cylinder = nw.new_node('GeometryNodeMeshCylinder',
+ input_kwargs={'Vertices': group_input.outputs["U Resolution"], 'Side Segments': subtract})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Name': 'uv_map', 3: cylinder.outputs["UV Map"]},
+ attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': store_named_attribute, 'Top': cylinder.outputs["Top"], 'Side': cylinder.outputs["Side"], 'Bottom': cylinder.outputs["Bottom"]},
+ attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_shifted_circle', singleton=False, type='GeometryNodeTree')
+def nodegroup_shifted_circle(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Resolution', 32),
+ ('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'Z', 0.0000),
+ ('NodeSocketFloat', 'Rot Z', 0.0000)])
+
+ curve_circle_3 = nw.new_node(Nodes.CurveCircle,
+ input_kwargs={'Resolution': group_input.outputs["Resolution"], 'Radius': group_input.outputs["Radius"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Z"]})
+
+ radians = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Rot Z"]}, attrs={'operation': 'RADIANS'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': radians})
+
+ transform_3 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curve_circle_3.outputs["Curve"], 'Translation': combine_xyz, 'Rotation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_3}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_shifted_square', singleton=False, type='GeometryNodeTree')
+def nodegroup_shifted_square(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Resolution', 10),
+ ('NodeSocketFloatDistance', 'Width', 1.0000),
+ ('NodeSocketFloat', 'Z', 0.0000),
+ ('NodeSocketFloat', 'Rot Z', 0.5000)])
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': group_input.outputs["Width"], 'Height': group_input.outputs["Width"]})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': quadrilateral, 'Count': group_input.outputs["Resolution"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Z"]})
+
+ radians = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Rot Z"]}, attrs={'operation': 'RADIANS'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': radians})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': resample_curve, 'Translation': combine_xyz, 'Rotation': combine_xyz_1})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Curve': transform_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_lofting', singleton=False, type='GeometryNodeTree')
+def nodegroup_lofting(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Profile Curves', None),
+ ('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 32),
+ ('NodeSocketBool', 'Use Nurb', False)])
+
+ cylinderside = nw.new_node(nodegroup_cylinder_side().name,
+ input_kwargs={'U Resolution': group_input.outputs["U Resolution"], 'V Resolution': group_input})
+
+ index = nw.new_node(Nodes.Index)
+
+ evaluate_on_domain = nw.new_node(Nodes.EvaluateonDomain, input_kwargs={1: index}, attrs={'domain': 'CURVE', 'data_type': 'INT'})
+
+ equal = nw.new_node(Nodes.Compare,
+ input_kwargs={2: evaluate_on_domain.outputs[1]},
+ attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+
+ curve_line = nw.new_node(Nodes.CurveLine)
+
+ domain_size = nw.new_node(Nodes.DomainSize, input_kwargs={'Geometry': group_input}, attrs={'component': 'CURVE'})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line, 'Count': domain_size.outputs["Spline Count"]})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': group_input, 'Selection': equal, 'Instance': resample_curve})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ flipindex = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': domain_size.outputs["Spline Count"], 'U Resolution': group_input.outputs["U Resolution"]})
+
+ sample_index_2 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': group_input, 3: position, 'Index': flipindex},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': realize_instances, 'Position': sample_index_2.outputs[2]})
+
+ set_spline_type_1 = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position}, attrs={'spline_type': 'CATMULL_ROM'})
+
+ set_spline_type = nw.new_node(Nodes.SplineType, input_kwargs={'Curve': set_position}, attrs={'spline_type': 'NURBS'})
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input.outputs["Use Nurb"], 14: set_spline_type_1, 15: set_spline_type})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': switch.outputs[6], 'Count': group_input})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ flipindex_1 = nw.new_node(nodegroup_flip_index().name,
+ input_kwargs={'V Resolution': group_input.outputs["U Resolution"], 'U Resolution': group_input})
+
+ sample_index_3 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve_1, 3: position_1, 'Index': flipindex_1},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': cylinderside.outputs["Geometry"], 'Position': sample_index_3.outputs[2]})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': set_position_1, 'Top': cylinderside.outputs["Top"], 'Side': cylinderside.outputs["Side"], 'Bottom': cylinderside.outputs["Bottom"]},
+ attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_warp_around_curve', singleton=False, type='GeometryNodeTree')
+def nodegroup_warp_around_curve(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketGeometry', 'Curve', None),
+ ('NodeSocketInt', 'U Resolution', 32),
+ ('NodeSocketInt', 'V Resolution', 32),
+ ('NodeSocketFloat', 'Radius', 1.0000)])
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve,
+ input_kwargs={'Curve': group_input.outputs["Curve"], 'Count': group_input.outputs["V Resolution"]})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ index = nw.new_node(Nodes.Index)
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: index, 1: group_input.outputs["U Resolution"]},
+ attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'FLOOR'})
+
+ sample_index_3 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: position_1, 'Index': floor},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ normal = nw.new_node(Nodes.InputNormal)
+
+ sample_index_5 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: normal, 'Index': floor},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ scale = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: sample_index_5.outputs[2], 'Scale': separate_xyz.outputs["X"]},
+ attrs={'operation': 'SCALE'})
+
+ curve_tangent = nw.new_node(Nodes.CurveTangent)
+
+ sample_index_4 = nw.new_node(Nodes.SampleIndex,
+ input_kwargs={'Geometry': resample_curve, 3: curve_tangent, 'Index': floor},
+ attrs={'data_type': 'FLOAT_VECTOR'})
+
+ cross_product = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: sample_index_4.outputs[2], 1: sample_index_5.outputs[2]},
+ attrs={'operation': 'CROSS_PRODUCT'})
+
+ scale_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: cross_product.outputs["Vector"], 'Scale': separate_xyz.outputs["Y"]},
+ attrs={'operation': 'SCALE'})
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: scale.outputs["Vector"], 1: scale_1.outputs["Vector"]})
+
+ scale_2 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: add.outputs["Vector"], 'Scale': group_input.outputs["Radius"]},
+ attrs={'operation': 'SCALE'})
+
+ add_1 = nw.new_node(Nodes.VectorMath, input_kwargs={0: sample_index_3.outputs[2], 1: scale_2.outputs["Vector"]})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': group_input.outputs["Geometry"], 'Position': add_1.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_position}, attrs={'is_active_output': True})
+
+def geometry_nodes(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ integer = nw.new_node(Nodes.Integer)
+ integer.integer = 32
+
+ shiftedsquare = nw.new_node(nodegroup_shifted_square().name, input_kwargs={'Resolution': integer})
+
+ shiftedcircle = nw.new_node(nodegroup_shifted_circle().name, input_kwargs={'Resolution': integer, 'Radius': 0.9200, 'Z': 2.5600})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': shiftedcircle, 'Rotation': (0.0000, 0.0000, 0.7854)})
+
+ shiftedsquare_1 = nw.new_node(nodegroup_shifted_square().name, input_kwargs={'Resolution': integer, 'Z': 10.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: integer, 1: 2.0000}, attrs={'operation': 'DIVIDE'})
+
+ star = nw.new_node('GeometryNodeCurveStar',
+ input_kwargs={'Points': divide, 'Inner Radius': 0.5000, 'Outer Radius': 0.6600})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': star.outputs["Curve"], 'Translation': (0.0000, 0.0000, 7.6000), 'Rotation': (0.0000, 0.0000, 0.7854)})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [shiftedsquare, transform_geometry, shiftedsquare_1, transform_geometry_1]})
+
+ v_resolution = nw.new_node(Nodes.Integer, label='V Resolution')
+ v_resolution.integer = 64
+
+ lofting = nw.new_node(nodegroup_lofting().name,
+ input_kwargs={'Profile Curves': join_geometry, 'U Resolution': integer, 'V Resolution': v_resolution})
+
+ object_info = nw.new_node(Nodes.ObjectInfo, input_kwargs={'Object': bpy.data.objects['BezierCurve']})
+
+ warparoundcurve = nw.new_node(nodegroup_warp_around_curve().name,
+ input_kwargs={'Geometry': lofting.outputs["Geometry"], 'Curve': object_info.outputs["Geometry"], 'U Resolution': integer, 'V Resolution': v_resolution})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': warparoundcurve}, attrs={'is_active_output': True})
+
+
+
+def apply(obj, selection=None, **kwargs):
+ surface.add_geomod(obj, geometry_nodes, selection=selection, attributes=[])
+apply(bpy.context.active_object)
\ No newline at end of file
diff --git a/infinigen/assets/tables/strechers.py b/infinigen/assets/tables/strechers.py
new file mode 100644
index 000000000..aa20b8987
--- /dev/null
+++ b/infinigen/assets/tables/strechers.py
@@ -0,0 +1,33 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_cylinder
+
+@node_utils.to_nodegroup('nodegroup_strecher', singleton=False, type='GeometryNodeTree')
+def nodegroup_strecher(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (1.0000, 0.0000, 1.0000), 'End': (1.0000, 0.0000, -1.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'N-gon', 32),
+ ('NodeSocketFloat', 'Profile Width', 0.200)])
+
+ ngoncylinder = nw.new_node(nodegroup_n_gon_cylinder().name,
+ input_kwargs={'Radius Curve': curve_line, 'Height': 1.0000, 'N-gon': group_input.outputs["N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Aspect Ratio': 1.0000, 'Resolution': 64})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': ngoncylinder.outputs["Mesh"]},
+ attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tables/table_top.py b/infinigen/assets/tables/table_top.py
new file mode 100644
index 000000000..6aa594807
--- /dev/null
+++ b/infinigen/assets/tables/table_top.py
@@ -0,0 +1,174 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_cylinder, nodegroup_create_cap
+from infinigen.core.tagging import tag_nodegroup
+from infinigen.core import tags as t
+
+@node_utils.to_nodegroup('nodegroup_capped_cylinder', singleton=False, type='GeometryNodeTree')
+def nodegroup_capped_cylinder(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Thickness', 0.5000),
+ ('NodeSocketFloat', 'Radius', 0.2000),
+ ('NodeSocketFloatDistance', 'Cap Flatness', 4.0000),
+ ('NodeSocketFloat', 'Fillet Radius Vertical', 0.4000),
+ ('NodeSocketFloat', 'Cap Relative Scale', 1.0000),
+ ('NodeSocketFloat', 'Cap Relative Z Offset', 0.0000),
+ ('NodeSocketInt', 'Resolution', 64)])
+
+ create_cap = nw.new_node(nodegroup_create_cap().name,
+ input_kwargs={'Radius': group_input.outputs["Cap Flatness"], 'Resolution': group_input.outputs["Resolution"]},
+ label='CreateCap')
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Thickness"], 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply, 1: group_input.outputs["Cap Relative Z Offset"]})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Radius"], 1: 0.5}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: group_input.outputs["Cap Relative Scale"]})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': create_cap, 'Translation': combine_xyz_5, 'Scale': add_1})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Radius"], 1: 1.0}, attrs={'operation': 'MULTIPLY'})
+
+ generatetabletop = nw.new_node(nodegroup_generate_table_top().name,
+ input_kwargs={'Thickness': multiply, 'N-gon': group_input.outputs["Resolution"], 'Profile Width': multiply_2, 'Aspect Ratio': 1.0000, 'Fillet Ratio': 0.0000, 'Fillet Radius Vertical': group_input.outputs["Fillet Radius Vertical"]})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_5, generatetabletop]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry_2}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_generate_table_top', singleton=False, type='GeometryNodeTree')
+def nodegroup_generate_table_top(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (1.0000, 0.0000, 1.0000), 'End': (1.0000, 0.0000, -1.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloat', 'Thickness', 0.5000),
+ ('NodeSocketInt', 'N-gon', 0),
+ ('NodeSocketFloat', 'Profile Width', 0.5000),
+ ('NodeSocketFloat', 'Aspect Ratio', 0.5000),
+ ('NodeSocketFloat', 'Fillet Ratio', 0.2000),
+ ('NodeSocketFloat', 'Fillet Radius Vertical', 0.0000)])
+
+ ngoncylinder = nw.new_node(nodegroup_n_gon_cylinder().name,
+ input_kwargs={'Radius Curve': curve_line, 'Height': group_input.outputs["Thickness"], 'N-gon': group_input.outputs["N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Aspect Ratio': group_input.outputs["Aspect Ratio"], 'Fillet Ratio': group_input.outputs["Fillet Ratio"], 'Profile Resolution': 512, 'Resolution': 10})
+
+ arc = nw.new_node('GeometryNodeCurveArc', input_kwargs={'Resolution': 4, 'Radius': 0.7071, 'Sweep Angle': 4.7124})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': arc.outputs["Curve"], 'Rotation': (0.0000, 0.0000, -0.7854)})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Rotation': (0.0000, 1.5708, 0.0000)})
+
+ transform_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_2, 'Translation': (0.0000, 0.5000, 0.0000)})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': group_input.outputs["Fillet Radius Vertical"], 'Z': 1.0000})
+
+ transform_4 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_3, 'Scale': combine_xyz})
+
+ fillet_curve = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': transform_4, 'Count': 8, 'Radius': group_input.outputs["Fillet Radius Vertical"], 'Limit Radius': True},
+ attrs={'mode': 'POLY'})
+
+ transform_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': fillet_curve, 'Rotation': (1.5708, 1.5708, 0.0000), 'Scale': group_input.outputs["Thickness"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': ngoncylinder.outputs["Profile Curve"], 'Profile Curve': transform_6})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Thickness"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_5 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_to_mesh, 'Translation': combine_xyz_1})
+
+ index = nw.new_node(Nodes.Index)
+
+ equal = nw.new_node(Nodes.Compare, input_kwargs={'A': index, 'B': 0}, attrs={'data_type': 'INT', 'operation': 'EQUAL'})
+
+ cap = tag_nodegroup(nw, ngoncylinder.outputs["Caps"], t.Subpart.SupportSurface, selection=equal)
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [transform_5, cap]})
+
+ flip_faces = nw.new_node(Nodes.FlipFaces, input_kwargs={'Mesh': join_geometry})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Thickness"]})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': flip_faces, 'Translation': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={
+ 'Geometry': transform_1,
+ 'Curve': ngoncylinder.outputs["Profile Curve"],
+ })
+
+def geometry_generate_table_top_wrapper(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Profile N-gon', kwargs['Profile N-gon']),
+ ('NodeSocketFloat', 'Profile Width', kwargs['Profile Width']),
+ ('NodeSocketFloat', 'Profile Aspect Ratio', kwargs['Profile Aspect Ratio']),
+ ('NodeSocketFloat', 'Profile Fillet Ratio', kwargs['Profile Fillet Ratio']),
+ ('NodeSocketFloat', 'Thickness', kwargs['Thickness']),
+ ('NodeSocketFloat', 'Vertical Fillet Ratio', kwargs['Vertical Fillet Ratio'])]
+ )
+
+ generatetabletop = nw.new_node(nodegroup_generate_table_top().name,
+ input_kwargs={'Thickness': group_input.outputs["Thickness"], 'N-gon': group_input.outputs["Profile N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Aspect Ratio': group_input.outputs["Profile Aspect Ratio"], 'Fillet Ratio': group_input.outputs["Profile Fillet Ratio"], 'Fillet Radius Vertical': group_input.outputs["Vertical Fillet Ratio"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': generatetabletop}, attrs={'is_active_output': True})
+
+class TableTopFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(TableTopFactory, self).__init__(factory_seed, coarse=coarse)
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters()
+
+ @staticmethod
+ def sample_parameters():
+ # all in meters
+ return {
+ 'Profile N-gon': 4,
+ 'Profile Width': 1.0,
+ 'Profile Aspect Ratio': 1.0,
+ 'Profile Fillet Ratio': 0.2000,
+ 'Thickness': 0.1000,
+ 'Vertical Fillet Ratio': 0.2000
+ }
+
+ def create_asset(self, **params):
+
+ bpy.ops.mesh.primitive_plane_add(
+ size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ surface.add_geomod(obj, geometry_generate_table_top_wrapper, apply=False, input_kwargs=self.params)
+
+ return obj
\ No newline at end of file
diff --git a/infinigen/assets/tables/table_utils.py b/infinigen/assets/tables/table_utils.py
new file mode 100644
index 000000000..94703f79e
--- /dev/null
+++ b/infinigen/assets/tables/table_utils.py
@@ -0,0 +1,514 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+
+import bpy
+import bpy
+import mathutils
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+@node_utils.to_nodegroup('nodegroup_n_gon_profile', singleton=False, type='GeometryNodeTree')
+def nodegroup_n_gon_profile(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Profile N-gon', 4),
+ ('NodeSocketFloat', 'Profile Width', 1.0000),
+ ('NodeSocketFloat', 'Profile Aspect Ratio', 1.0000),
+ ('NodeSocketFloat', 'Profile Fillet Ratio', 0.2000)])
+
+ value = nw.new_node(Nodes.Value)
+ value.outputs[0].default_value = 0.5000
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': group_input.outputs["Profile N-gon"], 'Radius': value})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: 3.1416, 1: group_input.outputs["Profile N-gon"]},
+ attrs={'operation': 'DIVIDE'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': divide})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': curve_circle.outputs["Curve"], 'Rotation': combine_xyz_1})
+
+ transform_2 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform, 'Rotation': (0.0000, 0.0000, -1.5708)})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Profile Aspect Ratio"], 1: group_input.outputs["Profile Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["Profile Width"], 'Y': multiply, 'Z': 1.0000})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_2, 'Scale': combine_xyz})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Profile Width"], 1: group_input.outputs["Profile Fillet Ratio"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ fillet_curve_1 = nw.new_node('GeometryNodeFilletCurve',
+ input_kwargs={'Curve': transform_1, 'Count': 8, 'Radius': multiply_1, 'Limit Radius': True},
+ attrs={'mode': 'POLY'})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Output': fillet_curve_1}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_n_gon_cylinder', singleton=False, type='GeometryNodeTree')
+def nodegroup_n_gon_cylinder(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Radius Curve', None),
+ ('NodeSocketFloat', 'Height', 0.5000),
+ ('NodeSocketInt', 'N-gon', 0),
+ ('NodeSocketFloat', 'Profile Width', 0.5000),
+ ('NodeSocketFloat', 'Aspect Ratio', 0.5000),
+ ('NodeSocketFloat', 'Fillet Ratio', 0.2000),
+ ('NodeSocketInt', 'Profile Resolution', 64),
+ ('NodeSocketInt', 'Resolution', 128)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_1})
+
+ set_curve_tilt = nw.new_node(Nodes.SetCurveTilt, input_kwargs={'Curve': curve_line, 'Tilt': 3.1416})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve,
+ input_kwargs={'Curve': set_curve_tilt, 'Count': group_input.outputs["Resolution"]})
+
+ spline_parameter_1 = nw.new_node(Nodes.SplineParameter)
+
+ capture_attribute = nw.new_node(Nodes.CaptureAttribute,
+ input_kwargs={'Geometry': resample_curve, 2: spline_parameter_1.outputs["Factor"]})
+
+ ngonprofile = nw.new_node(nodegroup_n_gon_profile().name,
+ input_kwargs={'Profile N-gon': group_input.outputs["N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Profile Aspect Ratio': group_input.outputs["Aspect Ratio"], 'Profile Fillet Ratio': group_input.outputs["Fillet Ratio"]})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve,
+ input_kwargs={'Curve': ngonprofile, 'Count': group_input.outputs["Profile Resolution"]})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': capture_attribute.outputs["Geometry"], 'Profile Curve': resample_curve_1, 'Fill Caps': True})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve,
+ input_kwargs={'Curves': group_input.outputs["Radius Curve"], 'Factor': capture_attribute.outputs[2]},
+ attrs={'use_all_curves': True})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': sample_curve.outputs["Position"]})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': separate_xyz.outputs["X"], 'Y': separate_xyz.outputs["Y"]})
+
+ length = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz}, attrs={'operation': 'LENGTH'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["X"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math,
+ input_kwargs={0: separate_xyz_2.outputs["Y"], 1: length.outputs["Value"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ attribute_statistic = nw.new_node(Nodes.AttributeStatistic,
+ input_kwargs={'Geometry': group_input.outputs["Radius Curve"], 2: separate_xyz_1.outputs["Z"]})
+
+ map_range = nw.new_node(Nodes.MapRange,
+ input_kwargs={'Value': separate_xyz.outputs["Z"], 1: attribute_statistic.outputs["Min"], 2: attribute_statistic.outputs["Max"], 3: multiply, 4: 0.0000})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_1, 'Y': multiply_2, 'Z': map_range.outputs["Result"]})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': curve_to_mesh, 'Position': combine_xyz_2})
+
+ index = nw.new_node(Nodes.Index)
+
+ domain_size = nw.new_node(Nodes.DomainSize, input_kwargs={'Geometry': curve_to_mesh})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: domain_size.outputs["Face Count"], 1: 2.0000},
+ attrs={'operation': 'SUBTRACT'})
+
+ less_than = nw.new_node(Nodes.Compare,
+ input_kwargs={2: index, 3: subtract},
+ attrs={'operation': 'LESS_THAN', 'data_type': 'INT'})
+
+ delete_geometry = nw.new_node(Nodes.DeleteGeometry,
+ input_kwargs={'Geometry': curve_to_mesh, 'Selection': less_than},
+ attrs={'domain': 'FACE'})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Mesh': set_position, 'Profile Curve': resample_curve_1, 'Caps': delete_geometry},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_generate_radius_curve', singleton=False, type='GeometryNodeTree')
+def nodegroup_generate_radius_curve(nw: NodeWrangler, curve_control_points):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': (1.0000, 0.0000, 1.0000), 'End': (1.0000, 0.0000, -1.0000)})
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketInt', 'Resolution', 128)])
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line, 'Count': group_input.outputs["Resolution"]})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': spline_parameter.outputs["Factor"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], curve_control_points)
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': float_curve, 'Y': 1.0000, 'Z': 1.0000})
+
+ multiply = nw.new_node(Nodes.VectorMath, input_kwargs={0: position, 1: combine_xyz_1}, attrs={'operation': 'MULTIPLY'})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': resample_curve, 'Position': multiply.outputs["Vector"]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_position}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_create_anchors', singleton=False, type='GeometryNodeTree')
+def nodegroup_create_anchors(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Profile N-gon', 0),
+ ('NodeSocketFloat', 'Profile Width', 0.5000),
+ ('NodeSocketFloat', 'Profile Aspect Ratio', 0.5000),
+ ('NodeSocketFloat', 'Profile Rotation', 0.0000)])
+
+ equal = nw.new_node(Nodes.Compare,
+ input_kwargs={2: group_input.outputs["Profile N-gon"], 3: 1},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ equal_1 = nw.new_node(Nodes.Compare,
+ input_kwargs={2: group_input.outputs["Profile N-gon"], 3: 2},
+ attrs={'operation': 'EQUAL', 'data_type': 'INT'})
+
+ ngonprofile = nw.new_node(nodegroup_n_gon_profile().name,
+ input_kwargs={'Profile N-gon': group_input.outputs["Profile N-gon"], 'Profile Width': group_input.outputs["Profile Width"], 'Profile Aspect Ratio': group_input.outputs["Profile Aspect Ratio"], 'Profile Fillet Ratio': 0.0000})
+
+ curve_to_points = nw.new_node(Nodes.CurveToPoints, input_kwargs={'Curve': ngonprofile}, attrs={'mode': 'EVALUATED'})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Profile Width"], 1: 0.3535},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Profile Width"], 1: -0.3535},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz, 'End': combine_xyz_1})
+
+ curve_to_points_1 = nw.new_node(Nodes.CurveToPoints, input_kwargs={'Curve': curve_line}, attrs={'mode': 'EVALUATED'})
+
+ switch_1 = nw.new_node(Nodes.Switch,
+ input_kwargs={1: equal_1, 14: curve_to_points.outputs["Points"], 15: curve_to_points_1.outputs["Points"]})
+
+ points = nw.new_node('GeometryNodePoints')
+
+ switch = nw.new_node(Nodes.Switch, input_kwargs={1: equal, 14: switch_1.outputs[6], 15: points})
+
+ set_point_radius = nw.new_node(Nodes.SetPointRadius, input_kwargs={'Points': switch.outputs[6]})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Profile Rotation"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': set_point_radius, 'Rotation': combine_xyz_2})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform}, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_create_legs_and_strechers', singleton=False, type='GeometryNodeTree')
+def nodegroup_create_legs_and_strechers(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Anchors', None),
+ ('NodeSocketBool', 'Keep Legs', False),
+ ('NodeSocketGeometry', 'Leg Instance', None),
+ ('NodeSocketFloat', 'Table Height', 0.0000),
+ ('NodeSocketFloat', 'Leg Bottom Relative Scale', 0.0000),
+ ('NodeSocketFloat', 'Leg Bottom Relative Rotation', 0.0000),
+ ('NodeSocketBool', 'Keep Odd Strechers', True),
+ ('NodeSocketBool', 'Keep Even Strechers', True),
+ ('NodeSocketGeometry', 'Strecher Instance', None),
+ ('NodeSocketInt', 'Strecher Index Increment', 0),
+ ('NodeSocketFloat', 'Strecher Relative Position', 0.5000),
+ ('NodeSocketFloat', 'Leg Bottom Offset', 0.0000),
+ ('NodeSocketBool', 'Align Leg X rot', False)])
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Table Height"]})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': group_input.outputs["Anchors"], 'Translation': combine_xyz})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Leg Bottom Offset"]})
+
+ subtract = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz, 1: combine_xyz_3}, attrs={'operation': 'SUBTRACT'})
+
+ subtract_1 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: position, 1: subtract.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate,
+ input_kwargs={'Vector': subtract_1.outputs["Vector"], 'Angle': group_input.outputs["Leg Bottom Relative Rotation"]},
+ attrs={'rotation_type': 'Z_AXIS'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': group_input.outputs["Leg Bottom Relative Scale"], 'Y': group_input.outputs["Leg Bottom Relative Scale"], 'Z': 1.0000})
+
+ multiply = nw.new_node(Nodes.VectorMath, input_kwargs={0: vector_rotate, 1: combine_xyz_4}, attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: position, 1: multiply.outputs["Vector"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ align_euler_to_vector = nw.new_node(Nodes.AlignEulerToVector, input_kwargs={'Vector': subtract_2}, attrs={'axis': 'Z'})
+
+ align_euler_to_vector_3 = nw.new_node(Nodes.AlignEulerToVector,
+ input_kwargs={'Rotation': align_euler_to_vector, 'Vector': position},
+ attrs={'pivot_axis': 'Z'})
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={0: group_input.outputs["Align Leg X rot"], 8: align_euler_to_vector, 9: align_euler_to_vector_3},
+ attrs={'input_type': 'VECTOR'})
+
+ length = nw.new_node(Nodes.VectorMath, input_kwargs={0: subtract_2}, attrs={'operation': 'LENGTH'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': 1.0000, 'Z': length.outputs["Value"]})
+
+ instance_on_points = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': transform, 'Instance': group_input.outputs["Leg Instance"], 'Rotation': switch.outputs[3], 'Scale': combine_xyz_2})
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: group_input.outputs["Keep Legs"], 15: realize_instances})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Strecher Relative Position"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: subtract_2, 'Scale': multiply_1}, attrs={'operation': 'SCALE'})
+
+ position_2 = nw.new_node(Nodes.InputPosition)
+
+ add = nw.new_node(Nodes.VectorMath, input_kwargs={0: scale.outputs["Vector"], 1: position_2})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': transform, 'Position': add.outputs["Vector"]})
+
+ index = nw.new_node(Nodes.Index)
+
+ modulo = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 2.0000}, attrs={'operation': 'MODULO'})
+
+ op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: modulo, 1: group_input.outputs["Keep Odd Strechers"]})
+
+ op_not = nw.new_node(Nodes.BooleanMath, input_kwargs={0: modulo}, attrs={'operation': 'NOT'})
+
+ op_and_1 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: group_input.outputs["Keep Even Strechers"], 1: op_not})
+
+ op_or = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_and, 1: op_and_1}, attrs={'operation': 'OR'})
+
+ domain_size = nw.new_node(Nodes.DomainSize, input_kwargs={'Geometry': transform}, attrs={'component': 'POINTCLOUD'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: domain_size.outputs["Point Count"], 1: group_input.outputs["Strecher Index Increment"]},
+ attrs={'operation': 'DIVIDE'})
+
+ equal = nw.new_node(Nodes.Compare, input_kwargs={0: divide, 1: 2.0000}, attrs={'operation': 'EQUAL'})
+
+ boolean = nw.new_node(Nodes.Boolean, attrs={'boolean': True})
+
+ index_1 = nw.new_node(Nodes.Index)
+
+ divide_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: domain_size.outputs["Point Count"], 1: 2.0000},
+ attrs={'operation': 'DIVIDE'})
+
+ less_than = nw.new_node(Nodes.Compare,
+ input_kwargs={2: index_1, 3: divide_1},
+ attrs={'operation': 'LESS_THAN', 'data_type': 'INT'})
+
+ switch_2 = nw.new_node(Nodes.Switch, input_kwargs={0: equal, 6: boolean, 7: less_than}, attrs={'input_type': 'BOOLEAN'})
+
+ op_and_2 = nw.new_node(Nodes.BooleanMath, input_kwargs={0: op_or, 1: switch_2.outputs[2]})
+
+ position_1 = nw.new_node(Nodes.InputPosition)
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: group_input.outputs["Strecher Index Increment"]})
+
+ modulo_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: add_1, 1: domain_size.outputs["Point Count"]},
+ attrs={'operation': 'MODULO'})
+
+ field_at_index = nw.new_node(Nodes.FieldAtIndex, input_kwargs={'Index': modulo_1, 3: position_1}, attrs={'data_type': 'FLOAT_VECTOR'})
+
+ subtract_3 = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: position_1, 1: field_at_index.outputs[2]},
+ attrs={'operation': 'SUBTRACT'})
+
+ align_euler_to_vector_1 = nw.new_node(Nodes.AlignEulerToVector, input_kwargs={'Vector': subtract_3.outputs["Vector"]}, attrs={'axis': 'Z'})
+
+ align_euler_to_vector_2 = nw.new_node(Nodes.AlignEulerToVector, input_kwargs={'Rotation': align_euler_to_vector_1}, attrs={'pivot_axis': 'Z'})
+
+ length_1 = nw.new_node(Nodes.VectorMath, input_kwargs={0: subtract_3.outputs["Vector"]}, attrs={'operation': 'LENGTH'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': 1.0000, 'Y': 1.0000, 'Z': length_1.outputs["Value"]})
+
+ instance_on_points_1 = nw.new_node(Nodes.InstanceOnPoints,
+ input_kwargs={'Points': set_position, 'Selection': op_and_2, 'Instance': group_input.outputs["Strecher Instance"], 'Rotation': align_euler_to_vector_2, 'Scale': combine_xyz_1})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': instance_on_points_1})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [switch_1.outputs[6], realize_instances_1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_create_cap', singleton=False, type='GeometryNodeTree')
+def nodegroup_create_cap(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketInt', 'Resolution', 64)])
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Radius"], 1: 257.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ uv_sphere = nw.new_node(Nodes.MeshUVSphere,
+ input_kwargs={'Segments': group_input.outputs["Resolution"], 'Rings': multiply, 'Radius': group_input.outputs["Radius"]})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': uv_sphere.outputs["Mesh"], 'Name': 'uv_map', 3: uv_sphere.outputs["UV Map"]},
+ attrs={'data_type': 'FLOAT_VECTOR', 'domain': 'CORNER'})
+
+ power = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Radius"], 1: 2.0000}, attrs={'operation': 'POWER'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: power, 1: 1.0000}, attrs={'operation': 'SUBTRACT'})
+
+ sqrt = nw.new_node(Nodes.Math, input_kwargs={0: subtract}, attrs={'operation': 'SQRT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: sqrt, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_1})
+
+ transform = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ less_than = nw.new_node(Nodes.Compare, input_kwargs={0: separate_xyz.outputs["Z"]}, attrs={'operation': 'LESS_THAN'})
+
+ delete_geometry = nw.new_node(Nodes.DeleteGeometry, input_kwargs={'Geometry': transform, 'Selection': less_than})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Mesh': delete_geometry}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_arc_top', singleton=False, type='GeometryNodeTree')
+def nodegroup_arc_top(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketFloatDistance', 'Diameter', 1.0000),
+ ('NodeSocketFloat', 'Sweep Angle', 180.0000)])
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Diameter"], 1: 2.0000}, attrs={'operation': 'DIVIDE'})
+
+ multiply_add = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Sweep Angle"], 2: -90.0000},
+ attrs={'operation': 'MULTIPLY_ADD'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: multiply_add, 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ radians = nw.new_node(Nodes.Math, input_kwargs={0: multiply}, attrs={'operation': 'RADIANS'})
+
+ radians_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Sweep Angle"]}, attrs={'operation': 'RADIANS'})
+
+ arc = nw.new_node('GeometryNodeCurveArc',
+ input_kwargs={'Resolution': 32, 'Radius': divide, 'Start Angle': radians, 'Sweep Angle': radians_1})
+
+ transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': arc.outputs["Curve"], 'Rotation': (1.5708, 0.0000, 0.0000)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_1}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_align_bottom_to_floor', singleton=False, type='GeometryNodeTree')
+def nodegroup_align_bottom_to_floor(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': group_input.outputs["Geometry"]})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': bounding_box.outputs["Min"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': group_input.outputs["Geometry"], 'Translation': combine_xyz})
+
+ group_output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': transform_geometry_1, 'Offset': multiply},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_bent', singleton=False, type='GeometryNodeTree')
+def nodegroup_bent(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketGeometry', 'Geometry', None),
+ ('NodeSocketFloat', 'Amount', -0.1000)])
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ length = nw.new_node(Nodes.VectorMath, input_kwargs={0: position}, attrs={'operation': 'LENGTH'})
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ multiply = nw.new_node(Nodes.Math,
+ input_kwargs={0: length.outputs["Value"], 1: separate_xyz.outputs["X"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math,
+ input_kwargs={0: multiply, 1: group_input.outputs["Amount"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ vector_rotate = nw.new_node(Nodes.VectorRotate, input_kwargs={'Vector': position, 'Angle': multiply_1})
+
+ set_position = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': group_input.outputs["Geometry"], 'Position': vector_rotate})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_position}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('nodegroup_merge_curve', singleton=False, type='GeometryNodeTree')
+def nodegroup_merge_curve(nw: NodeWrangler):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Curve', None)])
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Curve': group_input.outputs["Curve"]})
+
+ merge_by_distance = nw.new_node(Nodes.MergeByDistance, input_kwargs={'Geometry': curve_to_mesh_1})
+
+ mesh_to_curve = nw.new_node(Nodes.MeshToCurve, input_kwargs={'Mesh': merge_by_distance})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Curve': mesh_to_curve}, attrs={'is_active_output': True})
\ No newline at end of file
diff --git a/infinigen/assets/tableware/__init__.py b/infinigen/assets/tableware/__init__.py
new file mode 100644
index 000000000..d1d3c46f8
--- /dev/null
+++ b/infinigen/assets/tableware/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .spoon import SpoonFactory
+from .knife import KnifeFactory
+from .chopsticks import ChopsticksFactory
+from .fork import ForkFactory, SpatulaFactory
+from .pan import PanFactory
+from .pot import PotFactory
+from .cup import CupFactory
+from .wineglass import WineglassFactory
+from .plate import PlateFactory
+from .bowl import BowlFactory
+from .fruit_container import FruitContainerFactory
+from .bottle import BottleFactory
+from .can import CanFactory
+from .jar import JarFactory
+from .food_bag import FoodBagFactory
+from .food_box import FoodBoxFactory
+from .lid import LidFactory
+from .plant_container import PlantContainerFactory, LargePlantContainerFactory
diff --git a/infinigen/assets/tableware/base.py b/infinigen/assets/tableware/base.py
new file mode 100644
index 000000000..8940bc8f7
--- /dev/null
+++ b/infinigen/assets/tableware/base.py
@@ -0,0 +1,116 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core import surface
+from infinigen.core.util.math import FixedSeed
+
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils.decorate import read_co, write_attribute
+from infinigen.assets.utils.misc import assign_material
+from infinigen.assets.material_assignments import AssetList
+
+
+class TablewareFactory(AssetFactory):
+ is_fragile = False
+ allow_transparent = False
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.thickness = .01
+ material_assignments = AssetList['TablewareFactory'](fragile=self.is_fragile,
+ transparent=self.allow_transparent)
+
+ self.surface = material_assignments['surface'].assign_material()
+ self.inside_surface = material_assignments['inside'].assign_material()
+ self.guard_surface = material_assignments['guard'].assign_material()
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ self.scratch, self.edge_wear = material_assignments['wear_tear']
+
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.guard_depth = self.thickness
+ self.has_guard = False
+ self.has_inside = False
+ self.lower_thresh = uniform(.5, .8)
+ self.scale = 1.
+ self.metal_color = 'bw+natural'
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ raise NotImplementedError
+
+ def add_guard(self, obj, selection):
+ if not self.has_guard:
+ selection = False
+
+ def geo_guard(nw: NodeWrangler):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ normal = nw.new_node(Nodes.InputNormal)
+ x = nw.separate(nw.new_node(Nodes.InputPosition))[0]
+ sel = surface.eval_argument(nw, selection, x=x, normal=normal)
+ geometry, top, side = nw.new_node(
+ Nodes.ExtrudeMesh,
+ input_args=[geometry, sel, None, self.guard_depth,
+ False]
+ ).outputs[:3]
+ guard = nw.boolean_math('OR', top, side)
+ geometry = nw.new_node(
+ Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': geometry, 'Name': 'guard', 'Value': guard},
+ attrs={'domain': 'FACE'}
+ )
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+ surface.add_geomod(obj, geo_guard, apply=True)
+
+ @staticmethod
+ def make_double_sided(selection):
+ return lambda nw, x, normal: nw.boolean_math(
+ 'AND',
+ surface.eval_argument(nw, selection, x=x, normal=normal),
+ nw.compare(
+ 'GREATER_THAN',
+ nw.math('ABSOLUTE', nw.separate(normal)[-1]),
+ .8
+ )
+ )
+
+ def finalize_assets(self, assets):
+ assign_material(assets, [])
+ self.surface.apply(assets, metal_color=self.metal_color)
+ if self.has_inside:
+ self.inside_surface.apply(assets, selection='inside', clear=True, metal_color='bw+natural')
+ if self.has_guard:
+ self.guard_surface.apply(assets, selection='guard', metal_color=self.metal_color)
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+ def solidify_with_inside(self, obj, thickness):
+ max_z = np.max(read_co(obj)[:, -1])
+ obj.vertex_groups.new(name='inside_')
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=thickness, offset=1, shell_vertex_group='inside_')
+ write_attribute(obj, 'inside_', 'inside', 'FACE')
+
+ def inside(nw: NodeWrangler):
+ lower = nw.compare(
+ 'LESS_THAN', nw.separate(nw.new_node(Nodes.InputPosition))[-1],
+ max_z * self.lower_thresh
+ )
+ inside = nw.compare('GREATER_THAN', surface.eval_argument(nw, 'inside'), .8)
+ return nw.boolean_math('AND', inside, lower)
+
+ write_attribute(obj, inside, 'lower_inside', 'FACE')
+ obj.vertex_groups.remove(obj.vertex_groups['inside_'])
diff --git a/infinigen/assets/tableware/bottle.py b/infinigen/assets/tableware/bottle.py
new file mode 100644
index 000000000..7868fa4d4
--- /dev/null
+++ b/infinigen/assets/tableware/bottle.py
@@ -0,0 +1,146 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_co, subdivide_edge_ring, subsurf
+from infinigen.assets.utils.draw import spin
+from infinigen.assets.utils.object import join_objects, new_cylinder
+from infinigen.assets.utils.uv import wrap_front_back
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.materials import text
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class BottleFactory(AssetFactory):
+ z_neck_offset = .05
+ z_waist_offset = .15
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.z_length = uniform(.15, .25)
+ self.x_length = self.z_length * uniform(.15, .25)
+ self.x_cap = uniform(.3, .35)
+ self.bottle_type = np.random.choice(['beer', 'bordeaux', 'champagne', 'coke', 'vintage'])
+ self.bottle_width = uniform(.002, .005)
+ self.z_waist = 0
+ match self.bottle_type:
+ case 'beer':
+ self.z_neck = uniform(.5, .6)
+ self.z_cap = uniform(.05, .08)
+ neck_size = uniform(.06, .1)
+ neck_ratio = uniform(.4, .5)
+ self.x_anchors = [0, 1, 1, (neck_ratio + 1) / 2 + (1 - neck_ratio) / 2 * self.x_cap,
+ neck_ratio + (1 - neck_ratio) * self.x_cap, self.x_cap, self.x_cap, 0]
+ self.z_anchors = [0, 0, self.z_neck, self.z_neck + uniform(.6, .7) * neck_size,
+ self.z_neck + neck_size, 1 - self.z_cap, 1, 1]
+ self.is_vector = [0, 1, 1, 0, 1, 1, 1, 0]
+ case 'bordeaux':
+ self.z_neck = uniform(.6, .7)
+ self.z_cap = uniform(.1, .15)
+ neck_size = uniform(.1, .15)
+ self.x_anchors = 0, 1, 1, (1 + self.x_cap) / 2, self.x_cap, self.x_cap, 0
+ self.z_anchors = [0, 0, self.z_neck, self.z_neck + uniform(.6, .7) * neck_size,
+ self.z_neck + neck_size, 1, 1]
+ self.is_vector = [0, 1, 1, 0, 1, 1, 0]
+ case 'champagne':
+ self.z_neck = uniform(.4, .5)
+ self.z_cap = uniform(.05, .08)
+ self.x_anchors = [0, 1, 1, 1, (1 + self.x_cap) / 2, self.x_cap, self.x_cap, 0]
+ self.z_anchors = [0, 0, self.z_neck, self.z_neck + uniform(.08, .1),
+ self.z_neck + uniform(.15, .18), 1 - self.z_cap, 1, 1]
+ self.is_vector = [0, 1, 1, 0, 0, 1, 1, 0]
+ case 'coke':
+ self.z_waist = uniform(.4, .5)
+ self.z_neck = self.z_waist + uniform(.2, .25)
+ self.z_cap = uniform(.05, .08)
+ self.x_anchors = [0, uniform(.85, .95), 1, uniform(.85, .95), 1, 1, self.x_cap, self.x_cap,
+ 0]
+ self.z_anchors = [0, 0, uniform(.08, .12), uniform(.18, .25), self.z_waist, self.z_neck,
+ 1 - self.z_cap, 1, 1]
+ self.is_vector = [0, 1, 0, 0, 1, 1, 1, 1, 0]
+ case 'vintage':
+ self.z_waist = uniform(.1, .15)
+ self.z_neck = uniform(.7, .75)
+ self.z_cap = uniform(.0, .08)
+ x_lower = uniform(.85, .95)
+ self.x_anchors = [0, x_lower, (x_lower + 1) / 2, 1, 1, (self.x_cap + 1) / 2, self.x_cap,
+ self.x_cap, 0]
+ self.z_anchors = [0, 0, self.z_waist - uniform(.1, .15), self.z_waist, self.z_neck,
+ self.z_neck + uniform(.1, .2), 1 - self.z_cap, 1, 1]
+ self.is_vector = [0, 1, 0, 1, 1, 0, 1, 1, 0]
+
+ material_assignments = AssetList['BottleFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ self.wrap_surface = material_assignments['wrap_surface'].assign_material()
+ if self.wrap_surface == text.Text:
+ self.wrap_surface = text.Text(self.factory_seed, False)
+
+ self.cap_surface = material_assignments['cap_surface'].assign_material()
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ self.scratch, self.edge_wear = material_assignments['wear_tear']
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.texture_shared = uniform() < .2
+ self.cap_subsurf = uniform() < .5
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ bottle = self.make_bottle()
+ wrap = self.make_wrap(bottle)
+ cap = self.make_cap()
+ obj = join_objects([bottle, wrap, cap])
+
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+ def make_bottle(self):
+ x_anchors = np.array(self.x_anchors) * self.x_length
+ z_anchors = np.array(self.z_anchors) * self.z_length
+ anchors = x_anchors, 0, z_anchors
+ obj = spin(anchors, np.nonzero(self.is_vector)[0])
+ subsurf(obj, 1, True)
+ subsurf(obj, 1)
+ if self.bottle_width > 0:
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.bottle_width)
+ self.surface.apply(obj, translucent=True)
+ return obj
+
+ def make_wrap(self, bottle):
+ obj = new_cylinder(vertices=128)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if len(f.verts) > 4]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_ONLY')
+ bmesh.update_edit_mesh(obj.data)
+ subdivide_edge_ring(obj, 16)
+ z_max = self.z_neck - uniform(.02, self.z_neck_offset) * (self.z_neck - self.z_waist)
+ z_min = self.z_waist + uniform(.02, self.z_waist_offset) * (self.z_neck - self.z_waist)
+ radius = np.max(read_co(bottle)[:, 0]) + 2e-3
+ obj.scale = radius, radius, (z_max - z_min) * self.z_length
+ obj.location[-1] = z_min * self.z_length
+ butil.apply_transform(obj, True)
+ wrap_front_back(obj, self.wrap_surface, self.texture_shared)
+ return obj
+
+ def make_cap(self):
+ obj = new_cylinder(vertices=128)
+ obj.scale = [(self.x_cap + .1) * self.x_length, (self.x_cap + .1) * self.x_length,
+ (self.z_cap + .01) * self.z_length]
+ obj.location[-1] = (1 - self.z_cap) * self.z_length
+ butil.apply_transform(obj, loc=True)
+ subsurf(obj, 1, self.cap_subsurf)
+ self.cap_surface.apply(obj)
+ return obj
diff --git a/infinigen/assets/tableware/bowl.py b/infinigen/assets/tableware/bowl.py
new file mode 100644
index 000000000..f0e3e1f94
--- /dev/null
+++ b/infinigen/assets/tableware/bowl.py
@@ -0,0 +1,51 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.tableware.base import TablewareFactory
+from infinigen.assets.utils.decorate import subsurf, set_shade_smooth
+from infinigen.assets.utils.draw import spin
+from infinigen.assets.utils.object import new_bbox
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+
+
+class BowlFactory(TablewareFactory):
+ allow_transparent = True
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_end = .5
+ self.z_length = log_uniform(.4, .8)
+ self.z_bottom = log_uniform(.02, .05)
+ self.x_bottom = uniform(.2, .3) * self.x_end
+ self.x_mid = uniform(.8, .95) * self.x_end
+ self.has_guard = False
+ self.thickness = uniform(.01, .03)
+ self.has_inside = uniform(0, 1) < .5
+ self.scale = log_uniform(.15, .4)
+ self.edge_wear = None
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ radius = self.x_end * self.scale
+ return new_bbox(-radius, radius, -radius, radius, 0, self.z_length * self.scale)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = 0, self.x_bottom, self.x_bottom + 1e-3, self.x_bottom, self.x_mid, self.x_end
+ z_anchors = 0, 0, 0, self.z_bottom, self.z_length / 2, self.z_length
+ anchors = x_anchors, np.zeros_like(x_anchors), z_anchors
+ obj = spin(anchors, [2, 3], 16, 64)
+ subsurf(obj, 1)
+ self.solidify_with_inside(obj, self.thickness)
+ butil.modify_mesh(obj, 'BEVEL', width=self.thickness / 2, segments=np.random.randint(2, 5))
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ subsurf(obj, 1)
+ set_shade_smooth(obj)
+ return obj
diff --git a/infinigen/assets/tableware/can.py b/infinigen/assets/tableware/can.py
new file mode 100644
index 000000000..c41d60528
--- /dev/null
+++ b/infinigen/assets/tableware/can.py
@@ -0,0 +1,97 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+import shapely
+from numpy.random import uniform
+from shapely import Point, affinity
+
+from infinigen.assets.utils.decorate import write_co
+from infinigen.assets.utils.object import join_objects, new_circle, new_cylinder
+from infinigen.assets.utils.uv import wrap_four_sides
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core import surface
+from infinigen.assets.materials import text
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class CanFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.x_length = log_uniform(.05, .1)
+ self.z_length = self.x_length * log_uniform(.5, 2.5)
+ self.shape = np.random.choice(['circle', 'rectangle'])
+ self.skewness = uniform(1, 2.5) if uniform() < .5 else 1
+
+ material_assignments = AssetList["CanFactory"]()
+ self.surface = material_assignments["surface"].assign_material()
+ self.wrap_surface = material_assignments["wrap_surface"].assign_material()
+ if self.wrap_surface == text.Text:
+ self.wrap_surface = text.Text(self.factory_seed, False)
+
+ scratch_prob, edge_wear_prob = material_assignments["wear_tear_prob"]
+ self.scratch, self.edge_wear = material_assignments["wear_tear"]
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.texture_shared = uniform() < .2
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ coords = self.make_coords()
+ obj = new_circle(vertices=len(coords))
+ write_co(obj, np.array([[x, y, 0] for x, y in coords]))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.edge_face_add()
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.z_length)
+ surface.add_geomod(obj, self.geo_cap, apply=True)
+ self.surface.apply(obj)
+ wrap = self.make_wrap(coords)
+ obj = join_objects([obj, wrap])
+ return obj
+
+ @staticmethod
+ def geo_cap(nw: NodeWrangler):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ selection = nw.compare('GREATER_THAN',
+ nw.math('ABSOLUTE', nw.separate(nw.new_node(Nodes.InputNormal))[-1]), 1 - 1e-3)
+ geometry, top = nw.new_node(Nodes.ExtrudeMesh, [geometry, selection, None, 0]).outputs[:2]
+ geometry = nw.new_node(Nodes.ScaleElements,
+ input_kwargs={'Geometry': geometry, 'Selection': top, 'Scale': uniform(.96, .98)
+ })
+ geometry = nw.new_node(Nodes.ExtrudeMesh, [geometry, top, None, -uniform(.005, .01)]).outputs[0]
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+ def make_coords(self):
+ match self.shape:
+ case 'circle':
+ p = Point(0, 0).buffer(self.x_length, quad_segs=64)
+ case _:
+ side = self.x_length * uniform(.2, .8)
+ p = shapely.box(-side, -side, side, side).buffer(self.x_length - side, quad_segs=16)
+ p = affinity.scale(p, yfact=1 / self.skewness)
+ coords = p.boundary.segmentize(.01).coords[:][:-1]
+ return coords
+
+ def make_wrap(self, coords):
+ obj = new_cylinder(vertices=len(coords))
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if len(f.verts) > 4]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_ONLY')
+ bmesh.update_edit_mesh(obj.data)
+ lowest, highest = self.z_length * uniform(0, .1), self.z_length * uniform(.9, 1.)
+ write_co(obj, np.concatenate([np.array([[x, y, lowest], [x, y, highest]]) for x, y in coords]))
+ obj.scale = 1 + 1e-3, 1 + 1e-3, 1
+ butil.apply_transform(obj)
+ wrap_four_sides(obj, self.wrap_surface, self.texture_shared)
+ return obj
diff --git a/infinigen/assets/tableware/chopsticks.py b/infinigen/assets/tableware/chopsticks.py
new file mode 100644
index 000000000..0eef1c331
--- /dev/null
+++ b/infinigen/assets/tableware/chopsticks.py
@@ -0,0 +1,81 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.tableware.base import TablewareFactory
+from infinigen.assets.utils.decorate import subsurf, write_co
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.utils.object import join_objects, new_grid
+from infinigen.core.nodes.node_info import Nodes
+from infinigen.core.nodes.node_wrangler import NodeWrangler
+from infinigen.core import surface
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+
+from infinigen.core.util import blender as butil
+
+
+class ChopsticksFactory(TablewareFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.y_length = uniform(.01, .02)
+ self.y_shrink = log_uniform(.2, .8)
+ self.is_square = uniform(0, 1) < .5
+ self.has_guard = uniform(0, 1) < .4
+ self.x_guard = uniform(.4, .9)
+ self.guard_depth = 0.
+ self.pre_level = 2
+ self.scale = log_uniform(.2, .4)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_single()
+ if uniform(0, 1) < .6:
+ obj = self.make_parallel(obj)
+ else:
+ obj = self.make_crossed(obj)
+ return obj
+
+ def make_parallel(self, obj):
+ distance = log_uniform(self.y_length, .04)
+ if uniform(0, 1) < .5:
+ other = deep_clone_obj(obj)
+ obj.location[1] = distance
+ obj.rotation_euler[-1] = uniform(0, np.pi / 8)
+ other.location[1] = -distance
+ other.rotation_euler[-1] = -uniform(0, np.pi / 8)
+ else:
+ obj.location[0] = -1
+ butil.apply_transform(obj, loc=True)
+ other = deep_clone_obj(obj)
+ obj.location[1] = distance
+ obj.rotation_euler[-1] = -uniform(0, np.pi / 8)
+ other.location[1] = -distance
+ other.rotation_euler[-1] = uniform(0, np.pi / 8)
+ return join_objects([obj, other])
+
+ def make_crossed(self, obj):
+ other = deep_clone_obj(obj)
+ other.location = uniform(-.1, .2), uniform(-.2, .2), self.y_length
+ sign = np.sign(other.location[1])
+ other.rotation_euler[-1] = -sign * log_uniform(np.pi / 8, np.pi / 4)
+ return join_objects([obj, other])
+
+ def make_single(self):
+ n = int(1 / self.y_length)
+ obj = new_grid(x_subdivisions=n - 1, y_subdivisions=1)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.y_length * 2)
+ l = np.linspace(self.y_shrink, 1, n) * self.y_length
+ x = np.concatenate([np.linspace(0, 1, n)] * 4)
+ y = np.concatenate([-l, l, -l, l])
+ z = np.concatenate([l, l, -l, -l])
+ write_co(obj, np.stack([x, y, z], -1))
+ subsurf(obj, 2, self.is_square)
+ self.add_guard(obj, lambda nw, x: nw.compare('GREATER_THAN', x, self.x_guard))
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
diff --git a/infinigen/assets/tableware/cup.py b/infinigen/assets/tableware/cup.py
new file mode 100644
index 000000000..7a6cc1ea9
--- /dev/null
+++ b/infinigen/assets/tableware/cup.py
@@ -0,0 +1,136 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.tableware.base import TablewareFactory
+from infinigen.assets.materials import text
+from infinigen.assets.utils.decorate import read_co, remove_vertices, subsurf, write_attribute
+from infinigen.assets.utils.object import join_objects
+from infinigen.assets.utils.draw import spin
+from infinigen.assets.utils.uv import wrap_sides
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class CupFactory(TablewareFactory):
+ allow_transparent = True
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_end = .25
+ self.is_short = uniform(0, 1) < .5
+ if self.is_short:
+ self.is_profile_straight = uniform(0, 1) < .2
+ self.x_lowest = log_uniform(.6, .9)
+ self.depth = log_uniform(.25, .5)
+ self.has_guard = uniform(0, 1) < .8
+ else:
+ self.is_profile_straight = True
+ self.x_lowest = log_uniform(.9, 1.)
+ self.depth = log_uniform(.5, 1.)
+ self.has_guard = False
+ if self.is_profile_straight:
+ self.handle_location = uniform(.45, .65)
+ else:
+ self.handle_location = uniform(-.1, .3)
+ self.handle_type = 'shear' if uniform(0, 1) < .5 else 'round'
+ self.handle_radius = self.depth * uniform(.2, .4)
+ self.handle_inner_radius = self.handle_radius * log_uniform(.2, .3)
+ self.handle_taper_x = uniform(0, 2)
+ self.handle_taper_y = uniform(0, 2)
+ self.x_lower_ratio = log_uniform(.8, 1.)
+ self.thickness = log_uniform(.01, .04)
+ self.has_wrap = uniform() < .3
+ self.has_wrap = True
+ self.wrap_margin = uniform(.1, .2)
+
+ material_assignments = AssetList['CupFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ self.wrap_surface = material_assignments['wrap_surface'].assign_material()
+ if self.wrap_surface == text.Text:
+ self.wrap_surface = text.Text(self.factory_seed, False)
+ self.scratch = self.edge_wear = None
+
+ self.has_inside = uniform(0, 1) < .5
+ self.scale = log_uniform(.15, .3)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ if self.is_profile_straight:
+ x_anchors = 0, self.x_lowest * self.x_end, self.x_end
+ z_anchors = 0, 0, self.depth
+ else:
+ x_anchors = 0, self.x_lowest * self.x_end, (self.x_lowest + self.x_lower_ratio * (
+ 1 - self.x_lowest)) * self.x_end, self.x_end
+ z_anchors = 0, 0, self.depth * .5, self.depth
+ anchors = x_anchors, np.zeros_like(x_anchors), z_anchors
+ obj = spin(anchors, [1], 16)
+ subsurf(obj, 1)
+ butil.modify_mesh(obj, 'BEVEL', True, offset_type='PERCENT', width_pct=uniform(10, 50), segments=8)
+ if self.has_wrap:
+ wrap = self.make_wrap(obj)
+ else:
+ wrap = None
+ self.solidify_with_inside(obj, self.thickness)
+ handle_location = x_anchors[-2] * (1 - self.handle_location) + x_anchors[-1] * self.handle_location, \
+ 0, \
+ z_anchors[-2] * (1 - self.handle_location) + z_anchors[-1] * self.handle_location
+ angle_low = np.arctan((x_anchors[-1] - x_anchors[-2]) / (z_anchors[-1] - z_anchors[-2]))
+ angle_height = np.arctan((x_anchors[2] - x_anchors[1]) / (z_anchors[2] - z_anchors[1]))
+ handle_angle = uniform(angle_low, angle_height + 1e-3)
+ if self.has_guard:
+ obj = self.add_handle(obj, handle_location, handle_angle)
+ if self.has_wrap:
+ butil.select_none()
+ obj = join_objects([obj, wrap])
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def add_handle(self, obj, handle_location, handle_angle):
+ bpy.ops.mesh.primitive_torus_add(location=handle_location, major_radius=self.handle_radius,
+ minor_radius=self.handle_inner_radius)
+ handle = bpy.context.active_object
+ handle.rotation_euler = np.pi / 2, handle_angle, 0
+ butil.modify_mesh(handle, 'SIMPLE_DEFORM', deform_method='TAPER', angle=self.handle_taper_x,
+ deform_axis='X')
+ butil.modify_mesh(handle, 'SIMPLE_DEFORM', deform_method='TAPER', angle=self.handle_taper_y,
+ deform_axis='Y')
+ butil.modify_mesh(handle, 'BOOLEAN', object=obj, operation='DIFFERENCE')
+ butil.select_none()
+ objs = butil.split_object(handle)
+ i = np.argmax([np.max(read_co(o)[:, 0]) for o in objs])
+ handle = objs[i]
+ objs.remove(handle)
+ butil.delete(objs)
+ subsurf(handle, 1)
+ write_attribute(handle, lambda nw: 1, "guard", "FACE")
+ return join_objects([obj, handle])
+
+ def make_wrap(self, obj):
+ butil.select_none()
+ obj = deep_clone_obj(obj)
+ remove_vertices(obj, lambda x, y, z: (z / self.depth < self.wrap_margin) | (
+ z / self.depth > 1 - self.wrap_margin + uniform(.0, .1)) | (
+ np.abs(np.arctan2(y, x)) < np.pi * self.wrap_margin))
+ obj.scale = 1 + 1e-2, 1 + 1e-2, 1
+ butil.apply_transform(obj)
+ write_attribute(obj, lambda nw: 1, "text", "FACE")
+ return obj
+
+ def finalize_assets(self, assets):
+ super().finalize_assets(assets)
+ if self.has_wrap:
+ for obj in assets if isinstance(assets, list) else [assets]:
+ wrap_sides(obj, self.wrap_surface, 'u', 'v', 'z', selection='text')
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
\ No newline at end of file
diff --git a/infinigen/assets/tableware/food_bag.py b/infinigen/assets/tableware/food_bag.py
new file mode 100644
index 000000000..68e986d63
--- /dev/null
+++ b/infinigen/assets/tableware/food_bag.py
@@ -0,0 +1,83 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import text
+from infinigen.assets.utils.decorate import geo_extension, read_co, subdivide_edge_ring, subsurf, write_co
+from infinigen.assets.utils.object import new_base_cylinder
+from infinigen.assets.utils.uv import wrap_front_back
+from infinigen.core import surface
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import log_uniform
+from infinigen.assets.material_assignments import AssetList
+
+
+class FoodBagFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.length = uniform(.1, .3)
+ self.is_packet = uniform() < .6
+ if self.is_packet:
+ self.width = self.length * log_uniform(.6, 1.)
+ self.depth = self.width * uniform(.5, .8)
+ self.curve_profile = uniform(2, 4)
+ else:
+ self.width = self.length * log_uniform(.2, .4)
+ self.depth = self.width * uniform(.6, 1.)
+ self.curve_profile = uniform(4, 8)
+ self.extrude_length = uniform(.05, .1)
+ material_assignments = AssetList["FoodBagFactory"]()
+ self.surface = material_assignments["surface"].assign_material()
+ if self.surface == text.Text:
+ self.surface = self.surface(self.factory_seed)
+ self.texture_shared = uniform() < .2
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_base()
+ self.add_seal(obj)
+ self.build_uv(obj)
+ subsurf(obj, 2)
+ surface.add_geomod(obj, geo_extension, input_kwargs={'musgrave_dimensions': '2D'}, apply=True)
+ return obj
+
+ def make_base(self):
+ obj = new_base_cylinder()
+ subdivide_edge_ring(obj, 64)
+ obj.scale = self.width / 2, self.depth / 2, self.length / 2
+ butil.apply_transform(obj)
+ x, y, z = read_co(obj).T
+ ratio = 1 - (2 * np.abs(z) / self.length) ** self.curve_profile
+ write_co(obj, np.stack([x, ratio * y, z], -1))
+ butil.modify_mesh(obj, 'WELD', merge_threshold=1e-3)
+ return obj
+
+ def add_seal(self, obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ for i in [-1, 1]:
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bm.verts.ensure_lookup_table()
+ indices = np.nonzero(read_co(obj)[:, -1] * i >= self.length / 2 - 1e-3)[0]
+ for idx in indices:
+ bm.verts[idx].select_set(True)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, self.extrude_length * self.length * i)})
+
+ def build_uv(self, obj):
+ if not self.is_packet:
+ obj.rotation_euler[1] = np.pi / 2
+ butil.apply_transform(obj)
+ wrap_front_back(obj, self.surface, self.texture_shared)
+ if not self.is_packet:
+ obj.rotation_euler[1] = -np.pi / 2
+ butil.apply_transform(obj)
diff --git a/infinigen/assets/tableware/food_box.py b/infinigen/assets/tableware/food_box.py
new file mode 100644
index 000000000..e0b645d7a
--- /dev/null
+++ b/infinigen/assets/tableware/food_box.py
@@ -0,0 +1,37 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials import text
+from infinigen.assets.utils.object import new_cube
+from infinigen.assets.utils.uv import wrap_six_sides
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+
+
+class FoodBoxFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ dimensions = np.sort(log_uniform(.05, .3, 3)).tolist()
+ self.dimensions = np.array([dimensions[1], dimensions[0], dimensions[2]])
+ self.surface = text.Text(self.factory_seed)
+ self.texture_shared = uniform() < .4
+
+ def create_placeholder(self, **params):
+ obj = new_cube()
+ obj.scale = self.dimensions / 2
+ butil.apply_transform(obj)
+ return obj
+
+ def create_asset(self, placeholder, **params) -> bpy.types.Object:
+ obj = butil.copy(placeholder)
+ wrap_six_sides(obj, self.surface, self.texture_shared)
+ butil.modify_mesh(obj, 'BEVEL', width=.001)
+ return obj
diff --git a/infinigen/assets/tableware/fork.py b/infinigen/assets/tableware/fork.py
new file mode 100644
index 000000000..86c3e949b
--- /dev/null
+++ b/infinigen/assets/tableware/fork.py
@@ -0,0 +1,89 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import subsurf, write_co
+from infinigen.core.util.random import log_uniform
+from .base import TablewareFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils.object import new_grid
+
+
+class ForkFactory(TablewareFactory):
+ x_end = .15
+ is_fragile = True
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_length = log_uniform(.4, .8)
+ self.x_tip = uniform(.15, .2)
+ self.y_length = log_uniform(.05, .08)
+ self.z_depth = log_uniform(.02, .04)
+ self.z_offset = uniform(.0, .05)
+ self.thickness = log_uniform(.008, .015)
+ self.has_guard = uniform(0, 1) < .4
+ self.guard_type = 'round' if uniform(0, 1) < .6 else 'double'
+ self.n_cuts = np.random.randint(1, 3) if uniform(0, 1) < .3 else 3
+ self.guard_depth = log_uniform(.2, 1.) * self.thickness
+ self.scale = log_uniform(.15, .25)
+ self.has_cut = True
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = np.array(
+ [self.x_tip, uniform(-.04, -.02), -.08, -.12, -self.x_end, -self.x_end - self.x_length,
+ -self.x_end - self.x_length * log_uniform(1.2, 1.4)])
+ y_anchors = np.array([self.y_length * log_uniform(.8, 1.), self.y_length * log_uniform(1., 1.2),
+ self.y_length * log_uniform(.6, 1.), self.y_length * log_uniform(.2, .4),
+ log_uniform(.01, .02), log_uniform(.02, .05), log_uniform(.01, .02)])
+ z_anchors = np.array([0, -self.z_depth, -self.z_depth, 0, self.z_offset, self.z_offset + uniform(-.02, .04),
+ self.z_offset + uniform(-.02, 0)])
+ n = 2 * (self.n_cuts + 1)
+ obj = new_grid(x_subdivisions=len(x_anchors) - 1, y_subdivisions=n - 1)
+ x = np.concatenate([x_anchors] * n)
+ y = np.ravel(y_anchors[np.newaxis, :] * np.linspace(1, -1, n)[:, np.newaxis])
+ z = np.concatenate([z_anchors] * n)
+ write_co(obj, np.stack([x, y, z], -1))
+ if self.has_cut:
+ self.make_cuts(obj)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ subsurf(obj, 1)
+ selection = lambda nw, x: nw.compare('LESS_THAN', x, -self.x_end)
+ if self.guard_type == 'double':
+ selection = self.make_double_sided(selection)
+ self.add_guard(obj, selection)
+ subsurf(obj, 1)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def make_cuts(self, obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ front_verts = []
+ for v in bm.verts:
+ if abs(v.co[0] - self.x_tip) < 1e-3:
+ front_verts.append(v)
+ front_verts = sorted(front_verts, key=lambda v: v.co[1])
+ geom = []
+ for f in bm.faces:
+ vs = list(v for v in f.verts if v in front_verts)
+ if len(vs) == 2:
+ if min(front_verts.index(vs[0]), front_verts.index(vs[1])) % 2 == 1:
+ geom.append(f)
+ bmesh.ops.delete(bm, geom=geom, context="FACES")
+ bmesh.update_edit_mesh(obj.data)
+
+
+class SpatulaFactory(ForkFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(SpatulaFactory, self).__init__(factory_seed, coarse)
+ self.has_cut = False
+ self.z_depth = uniform(0, .05)
+ self.y_length = log_uniform(.08, .12)
diff --git a/infinigen/assets/tableware/fruit_container.py b/infinigen/assets/tableware/fruit_container.py
new file mode 100644
index 000000000..29b5638e1
--- /dev/null
+++ b/infinigen/assets/tableware/fruit_container.py
@@ -0,0 +1,71 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections.abc import Iterable, Sequence
+from functools import cached_property
+from statistics import mean
+
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.fruits.general_fruit import FruitFactoryGeneralFruit
+from infinigen.assets.tableware import BowlFactory, PotFactory
+from infinigen.assets.utils.decorate import read_co, write_co
+from infinigen.assets.utils.misc import make_normalized_factory, subclasses
+from infinigen.core.placement.factory import AssetFactory, make_asset_collection
+from infinigen.core.placement.instance_scatter import scatter_instances
+from infinigen.core.util.math import FixedSeed
+
+from infinigen.core.util import blender as butil
+
+class FruitCover:
+ def __init__(self, factory_seed=0):
+ with FixedSeed(factory_seed):
+ fruit_factory_fns = list(subclasses(FruitFactoryGeneralFruit).difference([FruitFactoryGeneralFruit]))
+ fruit_factory_fn = make_normalized_factory(np.random.choice(fruit_factory_fns))
+ self.col = make_asset_collection(fruit_factory_fn(np.random.randint(1e5)), name='fruit', n=5)
+ self.dimension = mean(mean(o.dimensions) for o in self.col.objects)
+ self.shrink_rate = max(self.dimension, 2.)
+
+ def apply(self, obj, selection=None):
+ for obj in obj if isinstance(obj, Iterable) else [obj]:
+ scale = uniform(.06, .08) / self.shrink_rate
+ scatter_instances(
+ base_obj=obj, collection=self.col, density=1e3,
+ min_spacing=scale * self.dimension * uniform(.5, .7), scale=scale,
+ scale_rand=uniform(0.1, 0.3), selection=selection,
+ ground_offset=self.dimension * .2 * scale, apply_geo=True, realize=True
+ )
+
+
+class FruitContainerFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(FruitContainerFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ base_factory_fns = [BowlFactory, PotFactory]
+ probs = np.array([1, 1])
+ base_factory_fn = np.random.choice(base_factory_fns, p=probs / probs.sum())
+ self.base_factory = base_factory_fn(factory_seed, coarse)
+ self.cover_seed = factory_seed
+
+ @cached_property
+ def cover(self):
+ return FruitCover(self.cover_seed)
+
+ def create_placeholder(self, **params):
+ box = self.base_factory.create_placeholder(**params)
+ co = read_co(box)
+ co[co[:, -1] > .02, -1] += .05
+ co[co[:, -1] < .02, -1] -= .01
+ write_co(box, co)
+ butil.apply_transform(box)
+ return box
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ return self.base_factory.create_asset(**params)
+
+ def finalize_assets(self, assets):
+ self.base_factory.finalize_assets(assets)
+ self.cover.apply(assets, selection='lower_inside')
diff --git a/infinigen/assets/tableware/jar.py b/infinigen/assets/tableware/jar.py
new file mode 100644
index 000000000..79a1e9598
--- /dev/null
+++ b/infinigen/assets/tableware/jar.py
@@ -0,0 +1,83 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_co, subsurf, write_attribute
+from infinigen.assets.utils.object import join_objects, new_circle, new_cylinder
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+class JarFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.z_length = uniform(.15, .2)
+ self.x_length = uniform(.03, .06)
+ self.thickness = uniform(.002, .004)
+ self.n_base = np.random.choice([4, 6, 64])
+ self.x_cap = uniform(.6, .9) * np.cos(np.pi / self.n_base)
+ self.z_cap = uniform(.05, .08)
+ self.z_neck = uniform(.15, .2)
+
+ material_assignments = AssetList["JarFactory"]()
+ self.surface = material_assignments["surface"].assign_material()
+ self.cap_surface = material_assignments["cap_surface"].assign_material()
+ scratch_prob, edge_wear_prob = material_assignments["wear_tear_prob"]
+ self.scratch, self.edge_wear = material_assignments["wear_tear"]
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ self.cap_subsurf = uniform() < .5
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = new_cylinder(vertices=self.n_base)
+ obj.scale = self.x_length, self.x_length, self.z_length
+ butil.apply_transform(obj, True)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ geom = [f for f in bm.faces if f.normal[-1] > .5]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_KEEP_BOUNDARY')
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ subsurf(obj, 2, True)
+ top = new_circle(location=(0, 0, 0))
+ top.scale = [self.x_cap * self.x_length] * 3
+ top.location[-1] = (1 + self.z_neck) * self.z_length
+ butil.apply_transform(top)
+ butil.select_none()
+ obj = join_objects([obj, top])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.bridge_edge_loops(number_cuts=5, profile_shape_factor=uniform(0, .1))
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.region_to_loop()
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, self.z_cap * self.z_length)})
+ subsurf(obj, 1)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+
+ cap = new_cylinder(vertices=64)
+ cap.scale = *([self.x_cap * self.x_length + 1e-3] * 2), self.z_cap * self.z_length
+ cap.location[-1] = (1 + self.z_neck + self.z_cap * uniform(.5, .8)) * self.z_length
+ butil.apply_transform(cap, True)
+ subsurf(obj, 1, self.cap_subsurf)
+ write_attribute(cap, 1, 'cap', 'FACE')
+ obj = join_objects([obj, cap])
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets, clear=uniform() < .5)
+ self.cap_surface.apply(assets, selection='cap')
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/tableware/knife.py b/infinigen/assets/tableware/knife.py
new file mode 100644
index 000000000..39a753c35
--- /dev/null
+++ b/infinigen/assets/tableware/knife.py
@@ -0,0 +1,95 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_co, subsurf, write_co
+from infinigen.core.util.random import log_uniform
+from .base import TablewareFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils.object import new_grid
+
+
+class KnifeFactory(TablewareFactory):
+ x_end = .5
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_length = log_uniform(.4, .7)
+ self.has_guard = uniform(0, 1) < .7
+ if self.has_guard:
+ self.y_length = log_uniform(.1, .5)
+ self.y_guard = self.y_length * log_uniform(.2, .4)
+ else:
+ self.y_length = log_uniform(.1, .2)
+ self.y_guard = self.y_length * log_uniform(.3, .5)
+ self.x_guard = uniform(0, .2)
+ self.has_tip = uniform(0, 1) < .7
+ self.thickness = log_uniform(.02, .03)
+ y_off_rand = uniform(0, 1)
+ self.y_offset = .2 if y_off_rand < 1 / 8 else .5 if y_off_rand < 1 / 4 else uniform(.2, .6)
+ self.guard_type = 'round' if uniform(0, 1) < .6 else 'double'
+ self.guard_depth = log_uniform(.2, 1.) * self.thickness
+ self.scale = log_uniform(.2, .3)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = np.array(
+ [self.x_end, uniform(.5, .8) * self.x_end, uniform(.3, .4) * self.x_end, 1e-3, 0, -1e-3, -2e-3,
+ -self.x_end * self.x_length + 1e-3, -self.x_end * self.x_length])
+ y_anchors = np.array(
+ [1e-3, self.y_length * log_uniform(.75, .95), self.y_length, self.y_length, self.y_length,
+ self.y_guard, self.y_guard, self.y_guard, self.y_guard])
+ if not self.has_guard:
+ indices = [0, 1, 2, 4, 5, 7, 8]
+ x_anchors = x_anchors[indices]
+ y_anchors = y_anchors[indices]
+ if self.has_tip:
+ indices = [0] + list(range(len(x_anchors)))
+ x_anchors = x_anchors[indices]
+ x_anchors[0] += 1e-3
+ y_anchors = y_anchors[indices]
+ y_anchors[1] += 3e-3
+
+ obj = new_grid(x_subdivisions=len(x_anchors) - 1, y_subdivisions=1)
+ x = np.concatenate([x_anchors] * 2)
+ y = np.concatenate([y_anchors, np.zeros_like(y_anchors)])
+ y[0::len(y_anchors)] += self.y_offset * self.y_length
+ if self.has_tip:
+ y[1::len(y_anchors)] += self.y_offset * self.y_length
+ y[2::len(y_anchors)] += self.y_offset * (self.y_length - y_anchors[2])
+ else:
+ y[1::len(y_anchors)] += self.y_offset * (self.y_length - y_anchors[1])
+ z = np.concatenate([np.zeros_like(x_anchors)] * 2)
+ write_co(obj, np.stack([x, y, z], -1))
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ self.make_knife_tip(obj)
+ subsurf(obj, 1)
+ selection = lambda nw, x: nw.compare('LESS_THAN', x, -self.x_guard * self.x_length * self.x_end)
+ if self.guard_type == 'double':
+ selection = self.make_double_sided(selection)
+ self.add_guard(obj, selection)
+ subsurf(obj, 1)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def make_knife_tip(self, obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ for e in bm.edges:
+ u, v = e.verts
+ x0, y0, z0 = u.co
+ x1, y1, z1 = v.co
+ if x0 >= 0 and x1 >= 0 and abs(x0 - x1) < 2e-4:
+ if y0 > self.y_offset * self.y_length and y1 > self.y_offset * self.y_length:
+ bmesh.ops.pointmerge(bm, verts=[u, v], merge_co=(u.co + v.co) / 2)
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.mesh.select_mode(type="EDGE")
+ bpy.ops.mesh.select_loose(extend=False)
+ bpy.ops.mesh.delete(type='EDGE')
diff --git a/infinigen/assets/tableware/lid.py b/infinigen/assets/tableware/lid.py
new file mode 100644
index 000000000..083c2249c
--- /dev/null
+++ b/infinigen/assets/tableware/lid.py
@@ -0,0 +1,123 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_center, subsurf, write_co
+from infinigen.assets.utils.draw import spin
+from infinigen.assets.utils.object import join_objects, new_cylinder, new_line
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed
+from infinigen.assets.material_assignments import AssetList
+
+
+class LidFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(LidFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.x_length = uniform(.08, .15)
+ self.z_height = self.x_length * uniform(0, .5)
+ self.thickness = uniform(.003, .005)
+ self.is_glass = uniform() < .5
+ self.hardware_type = None
+ self.rim_height = uniform(1, 2) * self.thickness
+ self.handle_type = np.random.choice(['handle', 'knob'])
+ if self.handle_type == 'knob':
+ self.handle_height = self.x_length * uniform(.1, .15)
+ else:
+ self.handle_height = self.x_length * uniform(.2, .25)
+ self.handle_radius = self.x_length * uniform(.15, .25)
+ self.handle_width = self.x_length * uniform(.25, .3)
+ self.handle_subsurf_level = np.random.randint(0, 3)
+
+ if self.is_glass:
+ material_assignments = AssetList['GlassLidFactory']()
+ else:
+ material_assignments = AssetList["LidFactory"]()
+ self.surface = material_assignments["surface"].assign_material()
+ self.rim_surface = material_assignments["rim_surface"].assign_material()
+ self.handle_surface = material_assignments["handle_surface"].assign_material()
+
+ scratch_prob, edge_wear_prob = material_assignments["wear_tear_prob"]
+ self.scratch, self.edge_wear = material_assignments["wear_tear"]
+ self.scratch = None if uniform() > scratch_prob else self.scratch
+ self.edge_wear = None if uniform() > edge_wear_prob else self.edge_wear
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = 0, .01, self.x_length / 2, self.x_length
+ z_anchors = self.z_height, self.z_height, self.z_height * uniform(.7, .8), 0
+ obj = spin((x_anchors, 0, z_anchors))
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=0)
+ butil.modify_mesh(obj, 'BEVEL', width=self.thickness / 2, segments=4)
+ self.surface.apply(obj, clear=True if self.is_glass else None, metal_color='bw+natural')
+ parts = [obj]
+ if self.is_glass:
+ parts.append(self.add_rim())
+ match self.handle_type:
+ case 'handle':
+ parts.append(self.add_handle(obj))
+ case _:
+ parts.append(self.add_knob())
+ obj = join_objects(parts)
+ return obj
+
+ def add_rim(self):
+ butil.select_none()
+ bpy.ops.mesh.primitive_torus_add(
+ major_radius=self.x_length, minor_radius=self.thickness / 2,
+ major_segments=128
+ )
+ obj = bpy.context.active_object
+ obj.scale[-1] = self.rim_height / self.thickness
+ butil.apply_transform(obj)
+ self.rim_surface.apply(obj)
+ return obj
+
+ def add_handle(self, obj):
+ center = read_center(obj)
+ i = np.argmin(np.abs(center[:, :2] - np.array([self.handle_width, 0])[np.newaxis, :]).sum(-1))
+ z_offset = center[i, -1]
+ obj = new_line(3)
+ write_co(
+ obj, np.array(
+ [[-self.handle_width, 0, 0], [-self.handle_width, 0, self.handle_height],
+ [self.handle_width, 0, self.handle_height], [self.handle_width, 0, 0]]
+ )
+ )
+ subsurf(obj, self.handle_subsurf_level)
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (0, self.thickness * 2, 0)})
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=0)
+ butil.modify_mesh(obj, 'BEVEL', width=self.thickness / 2, segments=4)
+ obj.location = 0, -self.thickness, z_offset
+ butil.apply_transform(obj, True)
+ self.handle_surface.apply(obj)
+ return obj
+
+ def add_knob(self):
+ obj = new_cylinder()
+ obj.scale = *([self.thickness * uniform(1, 2)] * 2), self.handle_height
+ obj.location[-1] = self.z_height
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'BEVEL', width=self.thickness / 2, segments=4)
+ top = new_cylinder()
+ top.scale = self.handle_radius, self.handle_radius, self.thickness * uniform(1, 2)
+ top.location[-1] = self.z_height + self.handle_height
+ butil.apply_transform(top, True)
+ butil.modify_mesh(top, 'BEVEL', width=self.thickness / 2, segments=4)
+ obj = join_objects([obj, top])
+ self.handle_surface.apply(obj)
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
diff --git a/infinigen/assets/tableware/pan.py b/infinigen/assets/tableware/pan.py
new file mode 100644
index 000000000..5e9c53b1a
--- /dev/null
+++ b/infinigen/assets/tableware/pan.py
@@ -0,0 +1,129 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Karhan Kayan: fix cutter bug
+
+import bpy
+import bmesh
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.util.random import log_uniform
+from .base import TablewareFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils.decorate import subsurf
+from infinigen.assets.utils.object import (
+ join_objects, new_base_circle, new_base_cylinder, origin2lowest,
+)
+from infinigen.assets.material_assignments import AssetList
+from ..utils.misc import assign_material
+
+
+class PanFactory(TablewareFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.r_expand = 1 if uniform(0, 1) < .2 else log_uniform(1., 1.2)
+ self.depth = log_uniform(.3, .8)
+ if self.r_expand == 1:
+ self.r_mid = log_uniform(1., 1.3)
+ else:
+ self.r_mid = 1 + (self.r_expand - 1) * (uniform(.5, .85) if uniform(0, 1) < .5 else .5)
+ self.has_handle = True
+ self.has_handle_hole = uniform() < .6
+ self.pre_level = 2
+ self.x_handle = log_uniform(1.2, 2.)
+ self.z_handle = self.x_handle * uniform(0, .2)
+ self.z_handle_mid = uniform(.6, .8) * self.z_handle
+ self.s_handle = log_uniform(.8, 1.2)
+ self.thickness = log_uniform(.04, .06)
+ self.has_guard = uniform(0, 1) < .8
+ self.x_guard = self.r_expand + uniform(0, .2) * self.x_handle
+ self.guard_type = 'round'
+ self.guard_depth = log_uniform(1., 2.) * self.thickness
+ material_assignments = AssetList['PanFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ self.inside_surface = material_assignments['inside'].assign_material()
+ if self.surface == self.inside_surface:
+ self.has_inside = uniform(0, 1) < .5
+ else:
+ self.has_inside = True
+ self.metal_color = None
+ self.scale = log_uniform(.1, .15)
+ self.scratch = self.edge_wear = None
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_base()
+ origin2lowest(obj, vertical=True)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def make_base(self):
+ n = 4 * int(log_uniform(4, 8))
+ base = new_base_circle(vertices=n)
+ middle = new_base_circle(vertices=n, )
+ middle.location[-1] = self.depth / 2
+ middle.scale = [self.r_mid] * 3
+ upper = new_base_circle(vertices=n)
+ upper.location[-1] = self.depth
+ upper.scale = [self.r_expand] * 3
+ butil.apply_transform(upper, loc=True)
+ obj = join_objects([base, middle, upper])
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.bridge_edge_loops()
+ bm = bmesh.from_edit_mesh(obj.data)
+ for v in bm.verts:
+ v.select_set(np.abs(v.co[-1]) < 1e-3)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.fill_grid(use_interp_simple=True, offset=np.random.randint(n // 4))
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ obj.rotation_euler[-1] = np.pi / n
+ butil.apply_transform(obj)
+ if self.has_handle:
+ self.add_handle(obj)
+ self.solidify_with_inside(obj, self.thickness)
+ selection = lambda nw, x: nw.compare('GREATER_THAN', x, self.x_guard)
+ self.add_guard(obj, selection)
+ subsurf(obj, 1, True)
+ subsurf(obj, 3)
+ if self.has_handle_hole:
+ self.add_handle_hole(obj)
+ return obj
+
+ def add_handle(self, obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type="EDGE")
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.edges.ensure_lookup_table()
+ m = []
+ for e in bm.edges:
+ u, v = e.verts
+ m.append(u.co[0] + v.co[0] + u.co[2] + v.co[2])
+ ri = np.argmax(m)
+ for e in bm.edges:
+ e.select_set(e.index == ri)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (self.x_handle * .5, 0, self.z_handle_mid)}
+ )
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (self.x_handle * .5, 0, (self.z_handle - self.z_handle_mid))}
+ )
+ bpy.ops.transform.resize(value=[self.s_handle] * 3)
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': (1e-3, 0, 0)})
+
+ def add_handle_hole(self, obj):
+ cutter = new_base_cylinder()
+ cutter.scale = *([uniform(.06, .1)] * 2), 1
+ cutter.location[0] = self.r_expand + uniform(.8, .9) * self.x_handle
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
diff --git a/infinigen/assets/tableware/plant_container.py b/infinigen/assets/tableware/plant_container.py
new file mode 100644
index 000000000..f2eb58358
--- /dev/null
+++ b/infinigen/assets/tableware/plant_container.py
@@ -0,0 +1,129 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.cactus import CactusFactory
+from infinigen.assets.monocot import MonocotFactory
+from infinigen.assets.mushroom import MushroomFactory
+from infinigen.assets.small_plants import FernFactory, SnakePlantFactory, SpiderPlantFactory, SucculentFactory
+from infinigen.assets.tableware import PotFactory
+from infinigen.assets.utils.decorate import (
+ read_edge_center, read_edge_direction, remove_vertices,
+ select_edges, subsurf,
+)
+from infinigen.assets.utils.object import center, join_objects, new_bbox, origin2lowest
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+from infinigen.core.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS
+
+
+class PlantPotFactory(PotFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(PlantPotFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.has_handle = self.has_bar = self.has_guard = False
+ self.depth = log_uniform(.5, 1.)
+ self.r_expand = uniform(1.1, 1.3)
+ alpha = uniform(.5, .8)
+ self.r_mid = (self.r_expand - 1) * alpha + 1
+ material_assignments = AssetList["PlantContainerFactory"]()
+ self.surface = material_assignments["surface"].assign_material()
+ self.scale = log_uniform(.08, .12)
+
+
+class PlantContainerFactory(AssetFactory):
+ plant_factories = [CactusFactory, MushroomFactory, FernFactory, SucculentFactory, SpiderPlantFactory,
+ SnakePlantFactory]
+
+ def __init__(self, factory_seed, coarse=False):
+ super(PlantContainerFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.base_factory = PlantPotFactory(self.factory_seed, coarse)
+ self.dirt_ratio = uniform(.7, .8)
+ material_assignments = AssetList["PlantContainerFactory"]()
+ self.dirt_surface = material_assignments["dirt_surface"].assign_material()
+ fn = np.random.choice(self.plant_factories)
+ self.plant_factory = fn(self.factory_seed)
+ self.side_size = self.base_factory.scale * self.base_factory.r_expand
+ self.top_size = uniform(.4, .6)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ return new_bbox(
+ -self.side_size,
+ self.side_size,
+ -self.side_size,
+ self.side_size,
+ -.02,
+ self.base_factory.depth * self.base_factory.scale + self.top_size
+ )
+
+ def create_asset(self, i, **params) -> bpy.types.Object:
+ obj = self.base_factory.create_asset(i=i, **params)
+ horizontal = np.abs(read_edge_direction(obj)[:, -1]) < .1
+
+ edge_center = read_edge_center(obj)
+ z = edge_center[:, -1]
+ dirt_z = self.dirt_ratio * self.base_factory.depth * self.base_factory.scale
+ idx = np.argmin(np.abs(z - dirt_z) - horizontal * 10)
+ radius = np.sqrt((edge_center[idx] ** 2)[:2].sum())
+
+ selection = np.zeros_like(z).astype(bool)
+ selection[idx] = True
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type="EDGE")
+ select_edges(obj, selection)
+ bpy.ops.mesh.loop_multi_select(ring=False)
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+
+ dirt_ = bpy.context.selected_objects[-1]
+ butil.select_none()
+ self.base_factory.finalize_assets(obj)
+ with butil.ViewportMode(dirt_, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.fill_grid()
+ subsurf(dirt_, 3)
+ self.dirt_surface.apply(dirt_)
+ butil.apply_modifiers(dirt_)
+
+ remove_vertices(dirt_, lambda x, y, z: np.sqrt(x ** 2 + y ** 2) > radius * 0.92)
+ dirt_.location[-1] -= .02
+
+ plant = self.plant_factory.spawn_asset(i=i, loc=(0, 0, 0), rot=(0, 0, 0))
+ origin2lowest(plant, approximate=True)
+ self.plant_factory.finalize_assets(plant)
+
+ scale = np.min(
+ np.array([self.side_size, self.side_size, self.top_size]) / np.max(
+ np.abs(np.array(plant.bound_box)), 0
+ )
+ )
+ plant.scale = [scale] * 3
+ plant.location[-1] = dirt_z
+
+ obj = join_objects([obj, plant, dirt_])
+ return obj
+
+
+class LargePlantContainerFactory(PlantContainerFactory):
+ plant_factories = [MonocotFactory]
+
+ def __init__(self, factory_seed, coarse=False):
+ super(LargePlantContainerFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.base_factory.depth = log_uniform(1., 1.5)
+ self.base_factory.scale = log_uniform(.15, .25)
+ self.side_size = self.base_factory.scale * uniform(1.5, 2.) * self.base_factory.r_expand
+ self.top_size = uniform(1, 1.5)
+ # if WALL_HEIGHT - 2*WALL_THICKNESS < 3:
+ # self.top_size = uniform(1.5, WALL_HEIGHT - 2*WALL_THICKNESS)
+ # else:
+ # self.top_size = uniform(1.5, 3)
+ # print(f"{self.side_size=} {self.top_size=} {WALL_THICKNESS=} {WALL_HEIGHT=}")
diff --git a/infinigen/assets/tableware/plate.py b/infinigen/assets/tableware/plate.py
new file mode 100644
index 000000000..3bcc8756c
--- /dev/null
+++ b/infinigen/assets/tableware/plate.py
@@ -0,0 +1,44 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.tableware.base import TablewareFactory
+from infinigen.assets.utils.decorate import subsurf
+from infinigen.assets.utils.draw import spin
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+
+
+class PlateFactory(TablewareFactory):
+ allow_transparent = True
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_end = .5
+ self.z_length = log_uniform(.05, .2)
+ self.x_mid = uniform(.3, 1.) * self.x_end
+ self.z_mid = uniform(.3, .8) * self.z_length
+ self.has_guard = False
+ self.pre_level = 1
+ self.thickness = uniform(.01, .03)
+ self.has_inside = uniform(0, 1) < .2
+ self.scale = log_uniform(.2, .4)
+ self.scratch = self.edge_wear = None
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = 0, self.x_mid, self.x_mid, self.x_end
+ z_anchors = 0, 0, self.z_mid, self.z_length
+ anchors = x_anchors, np.zeros_like(x_anchors), z_anchors
+ obj = spin(anchors, [1, 2], 4, 16)
+ butil.modify_mesh(obj, 'SUBSURF', render_levels=self.pre_level, levels=self.pre_level)
+ self.solidify_with_inside(obj, self.thickness)
+ subsurf(obj, 2)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
diff --git a/infinigen/assets/tableware/pot.py b/infinigen/assets/tableware/pot.py
new file mode 100644
index 000000000..be7e89a3c
--- /dev/null
+++ b/infinigen/assets/tableware/pot.py
@@ -0,0 +1,100 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import read_co, write_attribute, subsurf
+from infinigen.assets.utils.object import join_objects, new_bbox
+from infinigen.core.util.random import log_uniform
+from . import PanFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+
+
+class PotFactory(PanFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.depth = log_uniform(.6, 2.)
+ self.r_expand = 1
+ self.r_mid = 1
+ self.has_bar = uniform(0, 1) < .5
+ self.has_handle = not self.has_handle
+ self.has_guard = not self.has_bar
+ self.bar_height = self.depth * uniform(.75, .85)
+ self.bar_radius = log_uniform(.2, .3)
+ self.bar_x = 1 + uniform(-self.bar_radius, self.bar_radius) * .05
+ self.bar_inner_radius = log_uniform(.2, .4) * self.bar_radius
+ scale = log_uniform(.6, 1.5)
+ self.bar_scale = log_uniform(.6, 1.) * scale, 1 * scale, log_uniform(.6, 1.2) * scale
+ self.bar_taper = log_uniform(.3, .8)
+ self.bar_y_rotation = uniform(-np.pi / 6, 0)
+ self.bar_x_offset = self.bar_radius * uniform(-.1, .1)
+
+ self.guard_type = 'round'
+ self.guard_depth = log_uniform(.5, 1.) * self.thickness
+ self.scale = log_uniform(.1, .15)
+
+ def post_init(self):
+ self.has_handle = not self.has_bar
+ self.has_guard = not self.has_bar
+
+ self.bar_x = 1 + uniform(-self.bar_radius, self.bar_radius) * .05
+ self.bar_inner_radius = log_uniform(.2, .4) * self.bar_radius
+ self.bar_x_offset = self.bar_radius * uniform(-.1, .1)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_base()
+ if self.has_bar:
+ self.add_bar(obj)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ if self.has_bar:
+ radius_ = 1 + self.bar_x_offset + self.bar_radius + self.bar_inner_radius + self.thickness
+ obj = new_bbox(-radius_, radius_, -1 - self.thickness, 1 + self.thickness, 0, self.depth)
+ elif self.has_handle:
+ obj = new_bbox(
+ -1 - self.thickness, 1 + self.thickness + self.x_handle, -1 - self.thickness, 1 + self.thickness, 0,
+ self.depth
+ )
+ else:
+ obj = new_bbox(
+ -1 - self.thickness, 1 + self.thickness, -1 - self.thickness, 1 + self.thickness, 0, self.depth
+ )
+ obj.scale = (self.scale,) * 3
+ butil.apply_transform(obj)
+ return obj
+
+ def add_bar(self, obj):
+ bars = []
+ for side in [-1, 1]:
+ bpy.ops.mesh.primitive_torus_add(
+ location=(side * (1 + self.bar_x_offset), 0, self.bar_height),
+ major_radius=self.bar_radius, minor_radius=self.bar_inner_radius
+ )
+ bar = bpy.context.active_object
+ bar.scale = self.bar_scale
+ butil.modify_mesh(
+ bar, 'SIMPLE_DEFORM', deform_method='TAPER', angle=self.bar_taper,
+ deform_axis='X'
+ )
+ bar.rotation_euler = 0, self.bar_y_rotation, 0 if side == 1 else np.pi
+ butil.apply_transform(bar)
+
+ butil.modify_mesh(bar, 'BOOLEAN', object=obj, operation='DIFFERENCE')
+ butil.select_none()
+ objs = butil.split_object(bar)
+ i = np.argmax([np.max(read_co(o)[:, 0] * side) for o in objs])
+ bar = objs[i]
+ objs.remove(bar)
+ butil.delete(objs)
+ subsurf(bar, 1)
+ write_attribute(bar, lambda nw: 1, "guard", "FACE")
+ bars.append(bar)
+ return join_objects([obj, *bars])
diff --git a/infinigen/assets/tableware/spoon.py b/infinigen/assets/tableware/spoon.py
new file mode 100644
index 000000000..5570a1733
--- /dev/null
+++ b/infinigen/assets/tableware/spoon.py
@@ -0,0 +1,58 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.utils.decorate import subsurf, write_co
+from infinigen.core.util.random import log_uniform
+from .base import TablewareFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+from infinigen.assets.utils.object import new_grid
+
+
+class SpoonFactory(TablewareFactory):
+ x_end = .15
+ is_fragile = True
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_length = log_uniform(.2, .8)
+ self.y_length = log_uniform(.06, .12)
+ self.z_depth = log_uniform(.08, .25)
+ self.z_offset = uniform(.0, .05)
+ self.thickness = log_uniform(.008, .015)
+ self.has_guard = uniform(0, 1) < .4
+ self.guard_type = 'round' if uniform(0, 1) < .6 else 'double'
+ self.guard_depth = log_uniform(.2, 1.) * self.thickness
+ self.scale = log_uniform(.15, .25)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ x_anchors = np.array([log_uniform(.07, .25), 0, -.08, -.12, -self.x_end, -self.x_end - self.x_length,
+ -self.x_end - self.x_length * log_uniform(1.2, 1.4)])
+ y_anchors = np.array([self.y_length * log_uniform(.1, .8), self.y_length * log_uniform(1., 1.2),
+ self.y_length * log_uniform(.6, 1.), self.y_length * log_uniform(.2, .4),
+ log_uniform(.01, .02), log_uniform(.02, .05), log_uniform(.01, .02)])
+ z_anchors = np.array(
+ [0, 0, 0, 0, self.z_offset, self.z_offset + uniform(-.02, .04), self.z_offset + uniform(-.02, 0)])
+ obj = new_grid(x_subdivisions=len(x_anchors) - 1, y_subdivisions=2)
+ x = np.concatenate([x_anchors] * 3)
+ y = np.concatenate([y_anchors, np.zeros_like(y_anchors), -y_anchors])
+ z = np.concatenate([z_anchors] * 3)
+ x[len(x_anchors)] += .02
+ z[len(x_anchors) + 1] = -self.z_depth
+ write_co(obj, np.stack([x, y, z], -1))
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ subsurf(obj, 1)
+ selection = lambda nw, x: nw.compare('LESS_THAN', x, -self.x_end)
+ if self.guard_type == 'double':
+ selection = self.make_double_sided(selection)
+ self.add_guard(obj, selection)
+ subsurf(obj, 2)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+ return obj
diff --git a/infinigen/assets/tableware/wineglass.py b/infinigen/assets/tableware/wineglass.py
new file mode 100644
index 000000000..463efae45
--- /dev/null
+++ b/infinigen/assets/tableware/wineglass.py
@@ -0,0 +1,50 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.tableware.base import TablewareFactory
+from infinigen.assets.materials import glass
+from infinigen.assets.utils.decorate import subsurf
+from infinigen.assets.utils.draw import spin
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util import blender as butil
+
+
+class WineglassFactory(TablewareFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super().__init__(factory_seed, coarse)
+ with FixedSeed(factory_seed):
+ self.x_end = .25
+ self.z_length = log_uniform(.6, 2.)
+ self.z_cup = uniform(.3, .6) * self.z_length
+ self.z_mid = self.z_cup + uniform(.3, .5) * (self.z_length - self.z_cup)
+ self.x_neck = log_uniform(.01, .02)
+ self.x_top = self.x_end * log_uniform(1, 1.4)
+ self.x_mid = self.x_top * log_uniform(.9, 1.2)
+ self.has_guard = False
+ self.thickness = uniform(.01, .03)
+ self.surface = glass
+ self.scale = log_uniform(.1, .3)
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ z_bottom = self.z_length * log_uniform(.01, .05)
+ x_anchors = self.x_end, self.x_end / 2, self.x_neck, self.x_neck, self.x_mid, self.x_top
+ z_anchors = 0, z_bottom / 2, z_bottom, self.z_cup, self.z_mid, self.z_length
+ anchors = x_anchors, np.zeros_like(x_anchors), z_anchors
+ obj = spin(anchors, [0, 1, 2, 3], 4, 16)
+ subsurf(obj, 2)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness)
+ subsurf(obj, 1)
+ obj.scale = [self.scale] * 3
+ butil.apply_transform(obj)
+
+ with butil.SelectObjects(obj):
+ bpy.ops.object.shade_smooth()
+
+ return obj
diff --git a/infinigen/assets/trees/branch.py b/infinigen/assets/trees/branch.py
index f4d98d2af..b68eb8b9c 100644
--- a/infinigen/assets/trees/branch.py
+++ b/infinigen/assets/trees/branch.py
@@ -15,7 +15,7 @@
from infinigen.core.util.math import FixedSeed
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_surface_bump', singleton=False, type='GeometryNodeTree')
def nodegroup_surface_bump(nw: NodeWrangler):
diff --git a/infinigen/assets/trees/generate.py b/infinigen/assets/trees/generate.py
index 0553d9c1f..7102914f0 100644
--- a/infinigen/assets/trees/generate.py
+++ b/infinigen/assets/trees/generate.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Alexander Raistrick, Yiming Zuo, Alejandro Newell
+# Authors: Alexander Raistrick, Yiming Zuo, Alejandro Newell, Lingjie Mei
import pdb
@@ -32,9 +32,10 @@
from infinigen.core import surface
from infinigen.assets.weather.cloud.generate import CloudFactory
-from ..utils.decorate import write_attribute
+from infinigen.assets.utils.decorate import write_attribute
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from ..utils.misc import toggle_show, toggle_hide
logger = logging.getLogger(__name__)
@@ -172,6 +173,8 @@ def create_asset(self, placeholder, face_size, distance, **kwargs) -> bpy.types.
butil.parent_to(skeleton_obj, skin_obj, no_inverse=True)
tag_object(skin_obj, 'tree')
+ butil.apply_modifiers(skin_obj)
+
return skin_obj
@@ -272,11 +275,13 @@ def make_leaf_collection(seed,
col = make_asset_collection(child_factories, n_leaf, verbose=True, weights=weights)
# if leaf_surface is not None:
# leaf_surface.apply(list(col.objects))
+ toggle_show(col)
for obj in col.objects:
if decimate_rate > 0:
butil.modify_mesh(obj, 'DECIMATE', ratio=1.0-decimate_rate, apply=True)
butil.apply_transform(obj, rot=True, scale=True)
butil.apply_modifiers(obj)
+ toggle_hide(col)
return col
def random_leaf_collection(season, n=5):
diff --git a/infinigen/assets/trees/tree.py b/infinigen/assets/trees/tree.py
index 9de5e3e53..13f033528 100644
--- a/infinigen/assets/trees/tree.py
+++ b/infinigen/assets/trees/tree.py
@@ -18,7 +18,7 @@
from infinigen.core.nodes.node_wrangler import Nodes
from infinigen.core.util import blender as butil
-from ..utils.object import data2mesh, mesh2obj
+from infinigen.assets.utils.object import data2mesh, mesh2obj
C = bpy.context
D = bpy.data
diff --git a/infinigen/assets/trees/tree_flower.py b/infinigen/assets/trees/tree_flower.py
index a785b6b04..aee0973ee 100644
--- a/infinigen/assets/trees/tree_flower.py
+++ b/infinigen/assets/trees/tree_flower.py
@@ -19,7 +19,7 @@
from infinigen.core.placement.factory import AssetFactory
from infinigen.core.util import blender as butil, color
from infinigen.core.util.math import FixedSeed, dict_lerp
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_polar_to_cart_old', singleton=True)
def nodegroup_polar_to_cart_old(nw):
diff --git a/infinigen/assets/trees/utils/geometrynodes.py b/infinigen/assets/trees/utils/geometrynodes.py
index ebf505947..34f67732e 100644
--- a/infinigen/assets/trees/utils/geometrynodes.py
+++ b/infinigen/assets/trees/utils/geometrynodes.py
@@ -3,8 +3,6 @@
# Authors: Alejandro Newell
-
-import imp
import bpy
import numpy as np
diff --git a/infinigen/assets/trees/utils/materials.py b/infinigen/assets/trees/utils/materials.py
index 87d78b4e4..3ada36d39 100644
--- a/infinigen/assets/trees/utils/materials.py
+++ b/infinigen/assets/trees/utils/materials.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Alejandro Newell
+# Authors: Alejandro Newell, Lingjie Mei
import numpy as np
@@ -11,6 +11,7 @@
import bpy
+from infinigen.core.util.color import hsv2rgba
from . import helper
C = bpy.context
@@ -36,7 +37,7 @@ def init_color_material(color, prefix='', hsv_variance=[0,0,0],
nt = m.node_tree
color = np.array(color) + np.random.randn(3) * np.array(hsv_variance)
color = list(color.clip(0,1))
- color = (*colorsys.hsv_to_rgb(*color), 1)
+ color = (hsv2rgba(*color))
if is_emission:
out_node = nt.nodes.get('Material Output')
diff --git a/infinigen/assets/tropic_plants/leaf_banana_tree.py b/infinigen/assets/tropic_plants/leaf_banana_tree.py
index a682cbec6..4ad93aae5 100644
--- a/infinigen/assets/tropic_plants/leaf_banana_tree.py
+++ b/infinigen/assets/tropic_plants/leaf_banana_tree.py
@@ -22,7 +22,7 @@
shader_stem_material
)
from infinigen.core.util import blender as butil
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_nodegroup_apply_wave', singleton=False, type='GeometryNodeTree')
diff --git a/infinigen/assets/tropic_plants/leaf_palm_tree.py b/infinigen/assets/tropic_plants/leaf_palm_tree.py
index ba9aa21bf..a9842ce66 100644
--- a/infinigen/assets/tropic_plants/leaf_palm_tree.py
+++ b/infinigen/assets/tropic_plants/leaf_palm_tree.py
@@ -21,7 +21,7 @@
nodegroup_nodegroup_leaf_rotate_x,
shader_stem_material
)
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@node_utils.to_nodegroup('nodegroup_nodegroup_apply_wave', singleton=False, type='GeometryNodeTree')
def nodegroup_nodegroup_apply_wave(nw: NodeWrangler):
diff --git a/infinigen/assets/underwater/seaweed.py b/infinigen/assets/underwater/seaweed.py
index 175186fda..7d0e684ad 100644
--- a/infinigen/assets/underwater/seaweed.py
+++ b/infinigen/assets/underwater/seaweed.py
@@ -12,7 +12,8 @@
from numpy.random import uniform
from infinigen.assets.creatures.util.animation.driver_repeated import repeated_driver
-from infinigen.assets.utils.decorate import assign_material, read_co, subsurface2face_size, write_co
+from infinigen.assets.utils.decorate import read_co, subsurface2face_size, write_co
+from infinigen.assets.utils.misc import assign_material
from infinigen.assets.utils.draw import make_circular_interp
import infinigen.core.util.blender as butil
from infinigen.core.placement.factory import AssetFactory
@@ -21,10 +22,11 @@
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes
from infinigen.core import surface
-from infinigen.assets.utils.misc import build_color_ramp, log_uniform
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
-
+from infinigen.core.tagging import tag_object, tag_nodegroup
+from infinigen.core.nodes.node_utils import build_color_ramp
class SeaweedFactory(AssetFactory):
@@ -107,7 +109,7 @@ def shader_seaweed(nw: NodeWrangler, base_hue=.3):
v_perturb = log_uniform(1., 2)
def map_perturb(h, s, v):
- return *colorsys.hsv_to_rgb(h + h_perturb, s + s_perturb, v / v_perturb), 1.
+ return hsv2rgba(h + h_perturb, s + s_perturb, v / v_perturb)
subsurface_ratio = .01
roughness = .8
diff --git a/infinigen/assets/underwater/urchin.py b/infinigen/assets/underwater/urchin.py
index 239bc5d18..abd58b8ea 100644
--- a/infinigen/assets/underwater/urchin.py
+++ b/infinigen/assets/underwater/urchin.py
@@ -10,16 +10,18 @@
import infinigen.core.util.blender as butil
from infinigen.assets.creatures.util.animation.driver_repeated import repeated_driver
-from infinigen.assets.utils.object import new_icosphere
-from infinigen.assets.utils.decorate import assign_material, geo_extension, separate_loose
-from infinigen.assets.utils.misc import log_uniform
+from infinigen.assets.utils.object import new_icosphere, separate_loose
+from infinigen.assets.utils.decorate import geo_extension
+from infinigen.assets.utils.misc import assign_material
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core.placement.detail import adapt_mesh_resolution
from infinigen.core.placement.factory import AssetFactory
from infinigen.core import surface
from infinigen.core.util.math import FixedSeed
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
class UrchinFactory(AssetFactory):
@@ -88,7 +90,7 @@ def shader_spikes(nw: NodeWrangler, base_hue):
transmission = uniform(.95, .99)
subsurface = uniform(.1, .2)
roughness = uniform(.5, .8)
- color = *colorsys.hsv_to_rgb(base_hue, uniform(.5, 1.), log_uniform(.05, 1.)), 1
+ color = hsv2rgba(base_hue, uniform(.5, 1.), log_uniform(.05, 1.))
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
'Base Color': color,
'Roughness': roughness,
@@ -101,7 +103,7 @@ def shader_spikes(nw: NodeWrangler, base_hue):
@staticmethod
def shader_girdle(nw: NodeWrangler, base_hue):
roughness = uniform(.5, .8)
- color = *colorsys.hsv_to_rgb(base_hue, uniform(.4, .5), log_uniform(.02, .1)), 1
+ color = hsv2rgba(base_hue, uniform(.4, .5), log_uniform(.02, .1))
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
input_kwargs={'Base Color': color, 'Roughness': roughness})
return principled_bsdf
@@ -109,7 +111,7 @@ def shader_girdle(nw: NodeWrangler, base_hue):
@staticmethod
def shader_base(nw: NodeWrangler, base_hue):
roughness = uniform(.5, .8)
- color = *colorsys.hsv_to_rgb(base_hue, uniform(.8, 1.), log_uniform(.01, .02)), 1
+ color = hsv2rgba(base_hue, uniform(.8, 1.), log_uniform(.01, .02))
principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
input_kwargs={'Base Color': color, 'Roughness': roughness})
return principled_bsdf
diff --git a/infinigen/assets/utils/autobevel.py b/infinigen/assets/utils/autobevel.py
new file mode 100644
index 000000000..0565a9242
--- /dev/null
+++ b/infinigen/assets/utils/autobevel.py
@@ -0,0 +1,53 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.core.util import blender as butil
+from infinigen.core.util.random import random_general as rg
+
+class BevelSharp:
+
+ def __init__(
+ self,
+ mult=1,
+ angle_min_deg=70,
+ segments=None,
+ ):
+
+ self.amount = uniform(0.001, 0.006)
+ self.mult = mult
+ self.angle_min_deg = angle_min_deg
+
+ if segments is None:
+ segments = 4 if uniform() < 0 else 1
+ self.segments = segments
+
+ def __call__(self, obj):
+ butil.select_none()
+ butil.select(obj)
+ with butil.ViewportMode(obj, 'EDIT'):
+
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.tris_convert_to_quads()
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ angle = np.deg2rad(self.angle_min_deg)
+
+ bpy.ops.mesh.edges_select_sharp(
+ sharpness=angle
+ )
+
+ bpy.ops.mesh.bevel(
+ offset=self.amount * self.mult,
+ segments=self.segments,
+ affect='EDGES',
+ offset_type='WIDTH'
+ )
\ No newline at end of file
diff --git a/infinigen/assets/utils/bbox_from_mesh.py b/infinigen/assets/utils/bbox_from_mesh.py
new file mode 100644
index 000000000..eec1c42b9
--- /dev/null
+++ b/infinigen/assets/utils/bbox_from_mesh.py
@@ -0,0 +1,87 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+import numpy as np
+
+from infinigen.core.util import blender as butil
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.placement.factory import AssetFactory
+
+@node_utils.to_nodegroup('nodegroup_cube_from_corners', singleton=True)
+def nodegroup_cube_from_corners(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketVector', 'min_corner', (0.0000, 0.0000, 0.0000)),
+ ('NodeSocketVector', 'max_corner', (0.0000, 0.0000, 0.0000))])
+
+ subtract = nw.new_node(Nodes.VectorMath,
+ input_kwargs={0: group_input.outputs["max_corner"], 1: group_input.outputs["min_corner"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': subtract.outputs["Vector"]})
+
+ mix = nw.new_node(Nodes.Mix,
+ input_kwargs={4: group_input.outputs["min_corner"], 5: group_input.outputs["max_corner"]},
+ attrs={'data_type': 'VECTOR'})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': cube.outputs["Mesh"], 'Translation': mix.outputs[1]})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry})
+
+def union_all_bbox(obj: bpy.types.Object):
+
+ mins, maxs = None, None
+ for oc in butil.iter_object_tree(obj):
+ if not oc.type == 'MESH':
+ continue
+ points = butil.apply_matrix_world(oc, np.array(oc.bound_box))
+ pmins, pmaxs = points.min(axis=0), points.max(axis=0)
+ mins = pmins if mins is None else np.minimum(pmins, mins)
+ maxs = pmaxs if maxs is None else np.maximum(pmins, mins)
+
+ return mins, maxs
+
+def box_from_corners(min_corner, max_corner):
+
+ bbox = butil.modify_mesh(
+ butil.spawn_vert(),
+ 'NODES',
+ apply=True,
+ node_group=nodegroup_cube_from_corners(),
+ ng_inputs=dict(min_corner=min_corner, max_corner=max_corner)
+ )
+
+ return bbox
+
+def bbox_mesh_from_hipoly(gen: AssetFactory, inst_seed: int, use_pholder=False):
+
+ objs = []
+ objs.append(gen.spawn_placeholder(inst_seed, loc=(0,0,0), rot=(0,0,0)))
+ if not use_pholder:
+ objs.append(gen.spawn_asset(inst_seed, placeholder=objs[-1]))
+
+ min_corner, max_corner = union_all_bbox(objs[-1])
+
+ if (
+ min_corner is None or
+ max_corner is None or
+ np.abs(min_corner - max_corner).sum() < 1e-5
+ ):
+ raise ValueError(f'{gen} spawned {objs[-1].name=} with total bbox {min_corner, max_corner}, invalid')
+
+ bbox = box_from_corners(min_corner, max_corner)
+
+ cleanup = set()
+ for o in objs:
+ cleanup.update(butil.iter_object_tree(o))
+ butil.delete(list(cleanup))
+
+ bbox.name = f'{gen.__class__.__name__}({gen.factory_seed}).bbox_placeholder({inst_seed})'
+ return bbox
\ No newline at end of file
diff --git a/infinigen/assets/utils/decorate.py b/infinigen/assets/utils/decorate.py
index 7d5596086..b538cd71d 100644
--- a/infinigen/assets/utils/decorate.py
+++ b/infinigen/assets/utils/decorate.py
@@ -1,23 +1,26 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
-from statistics import mean
import logging
+from collections.abc import Iterable
-import bmesh
import bpy
+import bmesh
import numpy as np
from numpy.random import uniform
+from trimesh.points import remove_close
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
+from infinigen.core.surface import write_attr_data
from infinigen.core.util import blender as butil
-from infinigen.core.util.blender import select_none
+from infinigen.core.util.math import normalize
def multi_res(obj):
@@ -33,77 +36,21 @@ def geo_extension(nw: NodeWrangler, noise_strength=.2, noise_scale=2., musgrave_
pos = nw.new_node(Nodes.InputPosition)
direction = nw.scale(pos, nw.scalar_divide(1, nw.vector_math('LENGTH', pos)))
direction = nw.add(direction, uniform(-1, 1, 3))
- musgrave = nw.scalar_multiply(nw.scalar_add(
- nw.new_node(Nodes.MusgraveTexture, [direction], input_kwargs={'Scale': noise_scale},
- attrs={'musgrave_dimensions': musgrave_dimensions}), .25), noise_strength)
- geometry = nw.new_node(Nodes.SetPosition,
- input_kwargs={'Geometry': geometry, 'Offset': nw.scale(musgrave, pos)})
+ musgrave = nw.scalar_multiply(
+ nw.scalar_add(
+ nw.new_node(
+ Nodes.MusgraveTexture, [direction], input_kwargs={'Scale': noise_scale},
+ attrs={'musgrave_dimensions': musgrave_dimensions}
+ ), .25
+ ), noise_strength
+ )
+ geometry = nw.new_node(
+ Nodes.SetPosition,
+ input_kwargs={'Geometry': geometry, 'Offset': nw.scale(musgrave, pos)}
+ )
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
-def separate_loose(obj):
- select_none()
- objs = butil.split_object(obj)
- i = np.argmax([len(o.data.vertices) for o in objs])
- obj = objs[i]
- objs.remove(obj)
- butil.delete(objs)
- return obj
-
-
-def toggle_hide(obj, recursive=True):
- if obj.name in bpy.data.collections:
- for o in obj.objects:
- toggle_hide(o, recursive)
- else:
- obj.hide_set(True)
- obj.hide_render = True
- if recursive:
- for c in obj.children:
- toggle_hide(c)
-
-
-def toggle_show(obj, recursive=True):
- if obj.name in bpy.data.collections:
- for o in obj.objects:
- toggle_show(o, recursive)
- else:
- obj.hide_set(False)
- obj.hide_render = False
- if recursive:
- for c in obj.children:
- toggle_hide(c)
-
-
-def join_objects(obj):
- if not isinstance(obj, list):
- obj = [obj]
- if len(obj) == 1:
- return obj[0]
- bpy.context.view_layer.objects.active = obj[0]
- butil.select_none()
- butil.select(obj)
- bpy.ops.object.join()
- obj = bpy.context.active_object
- obj.location = 0, 0, 0
- obj.rotation_euler = 0, 0, 0
- obj.scale = 1, 1, 1
- return obj
-
-
-def assign_material(obj, material):
- if not isinstance(obj, list):
- obj = [obj]
- for o in obj:
- with butil.SelectObjects(o):
- while len(o.data.materials):
- bpy.ops.object.material_slot_remove()
- if not isinstance(material, list):
- material = [material]
- for m in material:
- o.data.materials.append(m)
-
-
def subsurface2face_size(obj, face_size):
arr = np.zeros(len(obj.data.polygons))
obj.data.polygons.foreach_get('area', arr)
@@ -119,12 +66,86 @@ def subsurface2face_size(obj, face_size):
butil.modify_mesh(obj, 'SUBSURF', levels=levels, render_levels=levels)
+def read_selected(obj, domain='VERT'):
+ match domain:
+ case 'VERT':
+ arr = np.zeros(len(obj.data.vertices), int)
+ obj.data.vertices.foreach_get('select', arr)
+ case 'EDGE':
+ arr = np.zeros(len(obj.data.edges), int)
+ obj.data.edges.foreach_get('select', arr)
+ case _:
+ arr = np.zeros(len(obj.data.faces), int)
+ obj.data.faces.foreach_get('select', arr)
+ return arr.ravel()
+
+
def read_co(obj):
arr = np.zeros(len(obj.data.vertices) * 3)
obj.data.vertices.foreach_get('co', arr)
return arr.reshape(-1, 3)
+def read_edges(obj):
+ arr = np.zeros(len(obj.data.edges) * 2, dtype=int)
+ obj.data.edges.foreach_get('vertices', arr)
+ return arr.reshape(-1, 2)
+
+
+def read_edge_center(obj):
+ return read_co(obj)[read_edges(obj).reshape(-1)].reshape(-1, 2, 3).mean(1)
+
+
+def read_edge_direction(obj):
+ cos = read_co(obj)[read_edges(obj).reshape(-1)].reshape(-1, 2, 3)
+ return normalize(cos[:, 1] - cos[:, 0])
+
+
+def read_edge_length(obj):
+ cos = read_co(obj)[read_edges(obj).reshape(-1)].reshape(-1, 2, 3)
+ return np.linalg.norm(cos[:, 1] - cos[:, 0], axis=-1)
+
+
+def read_center(obj):
+ arr = np.zeros(len(obj.data.polygons) * 3)
+ obj.data.polygons.foreach_get('center', arr)
+ return arr.reshape(-1, 3)
+
+
+def read_normal(obj):
+ arr = np.zeros(len(obj.data.polygons) * 3)
+ obj.data.polygons.foreach_get('normal', arr)
+ return arr.reshape(-1, 3)
+
+
+def read_area(obj):
+ arr = np.zeros(len(obj.data.polygons))
+ obj.data.polygons.foreach_get('area', arr)
+ return arr.reshape(-1)
+
+
+def read_loop_vertices(obj):
+ arr = np.zeros(len(obj.data.loops), dtype=int)
+ obj.data.loops.foreach_get('vertex_index', arr)
+ return arr.reshape(-1)
+
+
+def read_loop_edges(obj):
+ arr = np.zeros(len(obj.data.loops), dtype=int)
+ obj.data.loops.foreach_get('edge_index', arr)
+ return arr.reshape(-1)
+
+
+def read_uv(obj):
+ arr = np.zeros(len(obj.data.loops) * 2)
+ obj.data.uv_layers.active.data.foreach_get('uv', arr)
+ return arr.reshape(-1, 2)
+
+
+def write_uv(obj, arr):
+ obj.data.uv_layers.active.data.foreach_set('uv', arr.reshape(-1))
+
+
def read_base_co(obj):
dg = bpy.context.evaluated_depsgraph_get()
obj = obj.evaluated_get(dg)
@@ -135,7 +156,13 @@ def read_base_co(obj):
def write_co(obj, arr):
- obj.data.vertices.foreach_set('co', arr.reshape(-1))
+ try:
+ obj.data.vertices.foreach_set('co', arr.reshape(-1))
+ except RuntimeError as e:
+ raise RuntimeError(
+ f'Failed to set vertices.co on {obj.name=}. Object has {len(obj.data.vertices)} verts, '
+ f'{arr.shape=}'
+ ) from e
def read_material_index(obj):
@@ -144,22 +171,43 @@ def read_material_index(obj):
return arr
+def read_loop_starts(obj):
+ arr = np.zeros(len(obj.data.polygons), dtype=int)
+ obj.data.polygons.foreach_get('loop_start', arr)
+ return arr
+
+
+def read_loop_totals(obj):
+ arr = np.zeros(len(obj.data.polygons), dtype=int)
+ obj.data.polygons.foreach_get('loop_total', arr)
+ return arr
+
+
def write_material_index(obj, arr):
obj.data.polygons.foreach_set('material_index', arr.reshape(-1))
+def set_shade_smooth(obj):
+ write_attr_data(obj, 'use_smooth', np.ones(len(obj.data.polygons), dtype=int), 'INT', 'FACE')
+
+
def displace_vertices(obj, fn):
co = read_co(obj)
- x, y, z = co.T
- f = fn(x, y, z)
- for i in range(3):
- co[:, i] += f[i]
+ if not isinstance(fn, Iterable):
+ x, y, z = co.T
+ fn = fn(x, y, z)
+ for i in range(3):
+ co[:, i] += fn[i]
+ else:
+ co += fn
write_co(obj, co)
-def remove_vertices(obj, fn):
- x, y, z = read_co(obj).T
- to_delete = np.nonzero(fn(x, y, z))[0]
+def remove_vertices(obj, to_delete):
+ if not isinstance(to_delete, Iterable):
+ x, y, z = read_co(obj).T
+ to_delete = to_delete(x, y, z)
+ to_delete = np.nonzero(to_delete)[0]
with butil.ViewportMode(obj, 'EDIT'):
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
@@ -169,66 +217,107 @@ def remove_vertices(obj, fn):
return obj
-def write_attribute(obj, fn, name, domain="POINT"):
- def geo_attribute(nw: NodeWrangler):
- geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
- attr = surface.eval_argument(nw, fn, position=nw.new_node(Nodes.InputPosition))
- geometry = nw.new_node(
- Nodes.StoreNamedAttribute,
- input_kwargs={'Geometry': geometry, 'Name': name, 'Value': attr},
- attrs={'domain': domain})
- nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+def remove_edges(obj, to_delete):
+ if not isinstance(to_delete, Iterable):
+ x, y, z = read_edge_center(obj).T
+ to_delete = to_delete(x, y, z)
+ to_delete = np.nonzero(to_delete)[0]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.edges.ensure_lookup_table()
+ geom = [bm.edges[_] for _ in to_delete]
+ bmesh.ops.delete(bm, geom=geom, context='EDGES_FACES')
+ bmesh.update_edit_mesh(obj.data)
+ return obj
- surface.add_geomod(obj, geo_attribute, apply=True)
+def remove_faces(obj, to_delete, remove_loose=True):
+ if not isinstance(to_delete, Iterable):
+ x, y, z = read_center(obj).T
+ to_delete = to_delete(x, y, z)
+ to_delete = np.nonzero(to_delete)[0]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.faces.ensure_lookup_table()
+ geom = [bm.faces[_] for _ in to_delete]
+ bmesh.ops.delete(bm, geom=geom, context='FACES_ONLY')
+ bmesh.update_edit_mesh(obj.data)
+ if remove_loose:
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_loose()
+ bpy.ops.mesh.delete(type='EDGE')
+ return obj
-def treeify(obj):
- if len(obj.data.vertices) == 0:
- return obj
- obj = separate_loose(obj)
+def select_vertices(obj, to_select):
+ if not isinstance(to_select, Iterable):
+ x, y, z = read_co(obj).T
+ to_select = to_select(x, y, z)
+ to_select = np.nonzero(to_select)[0]
with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='VERT')
+ bpy.ops.mesh.select_all(action='DESELECT')
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
+ for i in to_select:
+ bm.verts[i].select_set(True)
+ bm.select_flush(False)
+ bmesh.update_edit_mesh(obj.data)
+ return obj
+
+
+def select_edges(obj, to_select):
+ if not isinstance(to_select, Iterable):
+ x, y, z = read_edge_center(obj).T
+ to_select = to_select(x, y, z)
+ to_select = np.nonzero(to_select)[0]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ bm = bmesh.from_edit_mesh(obj.data)
bm.edges.ensure_lookup_table()
- included = np.zeros(len(bm.verts))
- i = min((v.co[-1], i) for i, v in enumerate(bm.verts))[1]
- queue = [bm.verts[i]]
- included[i] = 1
- to_keep = []
- while queue:
- v = queue.pop()
- for e in v.link_edges:
- o = e.other_vert(v)
- if not included[o.index]:
- included[o.index] = 1
- to_keep.append(e)
- queue.append(o)
- bmesh.ops.delete(bm, geom=list(set(bm.edges).difference(to_keep)), context='EDGES')
+ for i in to_select:
+ bm.edges[i].select_set(True)
+ bm.select_flush(False)
bmesh.update_edit_mesh(obj.data)
return obj
-def fix_tree(obj):
- with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
- bpy.ops.mesh.remove_doubles()
+def select_faces(obj, to_select):
+ if not isinstance(to_select, Iterable):
+ x, y, z = read_center(obj).T
+ to_select = to_select(x, y, z)
+ to_select = np.nonzero(to_select)[0]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='DESELECT')
bm = bmesh.from_edit_mesh(obj.data)
- vertices_remove = []
- for v in bm.verts:
- if len(v.link_edges) == 1:
- o = v.link_edges[0].other_vert(v)
- if len(o.link_edges) > 2:
- vertices_remove.append(v)
- bmesh.ops.delete(bm, geom=vertices_remove)
+ bm.faces.ensure_lookup_table()
+ for i in to_select:
+ bm.faces[i].select_set(True)
+ bm.select_flush(False)
bmesh.update_edit_mesh(obj.data)
return obj
-def add_distance_to_boundary(obj):
+def write_attribute(obj, fn, name, domain="POINT", data_type='FLOAT'):
+ def geo_attribute(nw: NodeWrangler):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ attr = surface.eval_argument(nw, fn, position=nw.new_node(Nodes.InputPosition))
+ geometry = nw.new_node(
+ Nodes.StoreNamedAttribute,
+ input_kwargs={'Geometry': geometry, 'Name': name, 'Value': attr},
+ attrs={'domain': domain, 'data_type': data_type}
+ )
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+ surface.add_geomod(obj, geo_attribute, apply=True)
+
+
+def distance2boundary(obj):
with butil.ViewportMode(obj, 'EDIT'):
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.region_to_loop()
- vg = obj.vertex_groups.new(name='distance')
with butil.ViewportMode(obj, 'EDIT'):
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
@@ -248,6 +337,78 @@ def add_distance_to_boundary(obj):
d += 1
distance[distance < 0] = 0
distance /= max(d, 1)
- for i, d in enumerate(distance):
- vg.add([i], d, 'REPLACE')
+ write_attr_data(obj, 'distance', distance)
return distance
+
+
+def mirror(obj, axis=0):
+ obj.scale[axis] = -1
+ butil.apply_transform(obj)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.flip_normals()
+ return obj
+
+
+def subsurf(obj, levels, simple=False):
+ if levels > 0:
+ butil.modify_mesh(
+ obj, 'SUBSURF', levels=levels, render_levels=levels,
+ subdivision_type='SIMPLE' if simple else "CATMULL_CLARK"
+ )
+
+
+def subdivide_edge_ring(obj, cuts=64, axis=(0, 0, 1), **kwargs):
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.edges.ensure_lookup_table()
+ selected = np.abs((read_edge_direction(obj) * np.array(axis)[np.newaxis, :]).sum(1)) > 1 - 1e-3
+ edges = [bm.edges[i] for i in np.nonzero(selected)[0]]
+ bmesh.ops.subdivide_edgering(bm, edges=edges, cuts=int(cuts), **kwargs)
+ bmesh.update_edit_mesh(obj.data)
+
+
+def solidify(obj, axis, thickness):
+ axes = [0, 1, 2]
+ axes.remove(axis)
+ u = np.zeros(3)
+ u[axes[0]] = thickness
+ v = np.zeros(3)
+ v[axes[1]] = thickness
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(TRANSFORM_OT_translate={'value': u})
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={'value': v})
+ obj.location = -(u + v) / 2
+ butil.apply_transform(obj, True)
+ return obj
+
+
+def decimate(points, n):
+ dist = .1
+ ratio = 1.2
+ while True:
+ culled = remove_close(points, dist)[0]
+ if len(culled) <= n or dist > 10:
+ dist /= ratio
+ break
+ dist *= ratio
+ culled = remove_close(points, dist)[0]
+ return np.random.permutation(culled)[:n]
+
+
+def remove_duplicate_edges(obj):
+ remove_faces(obj, np.ones_like(len(obj.data.polygons)), remove_loose=False)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.verts.ensure_lookup_table()
+ counts = []
+ for v in bm.verts:
+ counts.append(len(v.link_edges))
+ counts = np.array(counts)
+ u, v = read_edges(obj).T
+ to_delete = (counts[u] > 2) & (counts[v] > 2)
+ remove_edges(obj, to_delete)
diff --git a/infinigen/assets/utils/draw.py b/infinigen/assets/utils/draw.py
index adc9b92ac..1f6f221cc 100644
--- a/infinigen/assets/utils/draw.py
+++ b/infinigen/assets/utils/draw.py
@@ -1,21 +1,22 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
from collections.abc import Sized
-import bmesh
import bpy
+import bmesh
import numpy as np
from numpy.random import uniform
from scipy.interpolate import interp1d
-from infinigen.assets.utils.decorate import read_co, remove_vertices, separate_loose, write_attribute, write_co
+from infinigen.assets.utils.decorate import read_co, remove_vertices, write_attribute, write_co
from infinigen.assets.utils.mesh import polygon_angles
from infinigen.assets.utils.misc import make_circular, make_circular_angle
-from infinigen.assets.utils.object import data2mesh, mesh2obj
+from infinigen.assets.utils.object import data2mesh, mesh2obj, separate_loose
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.placement.detail import sharp_remesh_with_attrs
from infinigen.core.surface import read_attr_data
@@ -61,37 +62,73 @@ def surface_from_func(fn, div_x=16, div_y=16, size_x=2, size_y=2):
return mesh
-def bezier_curve(anchors, vector_locations=(), resolution=64):
+def bezier_curve(anchors, vector_locations=(), resolution=64, to_mesh=True):
n = [len(r) for r in anchors if isinstance(r, Sized)][0]
anchors = np.array([np.array(r, dtype=float) if isinstance(r, Sized) else np.full(n, r) for r in anchors])
bpy.ops.curve.primitive_bezier_curve_add(location=(0, 0, 0))
obj = bpy.context.active_object
- with butil.ViewportMode(obj, 'EDIT'):
- bpy.ops.curve.subdivide(number_cuts=n - 2)
- points = obj.data.splines[0].bezier_points
- for i in range(n):
- points[i].co = anchors[:, i]
- for i in range(n):
- points[i].handle_left_type = 'AUTO'
- points[i].handle_right_type = 'AUTO'
- for i in vector_locations:
+ if n > 2:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.curve.subdivide(number_cuts=n - 2)
+ points = obj.data.splines[0].bezier_points
+ for i in range(n):
+ points[i].co = anchors[:, i]
+ for i in range(n):
+ if i in vector_locations:
points[i].handle_left_type = 'VECTOR'
points[i].handle_right_type = 'VECTOR'
+ else:
+ points[i].handle_left_type = 'AUTO'
+ points[i].handle_right_type = 'AUTO'
obj.data.splines[0].resolution_u = resolution
+ if to_mesh:
+ return curve2mesh(obj)
+ return obj
+
+
+def curve2mesh(obj):
with butil.SelectObjects(obj):
bpy.ops.object.convert(target='MESH')
+ obj = bpy.context.active_object
butil.modify_mesh(obj, 'WELD', merge_threshold=1e-4)
- return bpy.context.active_object
+ return obj
-def remesh_fill(obj):
+def align_bezier(anchors, axes=None, scale=None, vector_locations=(), resolution=64, to_mesh=True):
+ obj = bezier_curve(anchors, vector_locations, resolution, False)
+ points = obj.data.splines[0].bezier_points
+ if scale is None:
+ scale = np.ones(2 * len(points) - 2)
+ if axes is None:
+ axes = [None] * len(points)
+ scale = [1, *scale, 1]
+ for i, p in enumerate(points):
+ a = axes[i]
+ if a is None:
+ continue
+ a = np.array(a)
+ p.handle_left_type = 'FREE'
+ p.handle_right_type = 'FREE'
+ proj_left = np.array(p.handle_left - p.co) @ a * a
+ p.handle_left = np.array(p.co) + proj_left / np.linalg.norm(proj_left) * np.linalg.norm(
+ p.handle_left - p.co) * scale[2 * i]
+ proj_right = np.array(p.handle_right - p.co) @ a * a
+ p.handle_right = np.array(p.co) + proj_right / np.linalg.norm(proj_right) * np.linalg.norm(
+ p.handle_right - p.co) * scale[2 * i + 1]
+ if to_mesh:
+ return curve2mesh(obj)
+ return obj
+
+
+def remesh_fill(obj, resolution=.005):
n = len(obj.data.vertices)
butil.modify_mesh(obj, 'SOLIDIFY', thickness=.1)
write_attribute(obj, lambda nw, position: nw.compare('GREATER_EQUAL', nw.new_node(Nodes.Index), n), 'top')
- sharp_remesh_with_attrs(obj, .005)
- is_top = read_attr_data(obj, 'top')[:, 0] > 1e-3
+ sharp_remesh_with_attrs(obj, resolution)
+ is_top = read_attr_data(obj, 'top') > 1e-3
remove_vertices(obj, lambda x, y, z: is_top)
+ obj.data.attributes.remove(obj.data.attributes['top'])
return obj
@@ -101,7 +138,7 @@ def spin(anchors, vector_locations=(), subdivision=64, resolution=None, axis=(0,
co = read_co(obj)
max_radius = np.amax(np.linalg.norm(co - (co @ np.array(axis))[:, np.newaxis] * np.array(axis), axis=-1))
if resolution is None: resolution = min(int(2 * np.pi * max_radius / .005), 128)
- butil.modify_mesh(obj, 'WELD', merge_threshold=.001)
+ butil.modify_mesh(obj, 'WELD', merge_threshold=1e-4)
if loop:
with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
bpy.ops.mesh.select_all(action='SELECT')
@@ -110,7 +147,8 @@ def spin(anchors, vector_locations=(), subdivision=64, resolution=None, axis=(0,
with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.spin(steps=resolution, angle=np.pi * 2, axis=axis, dupli=dupli)
- bpy.ops.mesh.remove_doubles(threshold=.001)
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.remove_doubles(threshold=1e-4)
return obj
diff --git a/infinigen/assets/utils/extract_nodegroup_parts.py b/infinigen/assets/utils/extract_nodegroup_parts.py
new file mode 100644
index 000000000..6a9a138e1
--- /dev/null
+++ b/infinigen/assets/utils/extract_nodegroup_parts.py
@@ -0,0 +1,43 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+
+from infinigen.core.util import blender as butil
+
+from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes, geometry_node_group_empty_new
+
+def extract_nodegroup_geo(target_obj, nodegroup, k, ng_params=None):
+
+ assert k in nodegroup.outputs
+ assert target_obj.type == 'MESH'
+
+ vert = butil.spawn_vert('extract_nodegroup_geo.temp')
+
+ butil.modify_mesh(vert, type='NODES', apply=False)
+ if vert.modifiers[0].node_group == None:
+ group = geometry_node_group_empty_new()
+ vert.modifiers[0].node_group = group
+ ng = vert.modifiers[0].node_group
+ nw = NodeWrangler(ng)
+ obj_inp = nw.new_node(Nodes.ObjectInfo, [target_obj])
+
+ group_input_kwargs = {**ng_params}
+ if 'Geometry' in nodegroup.inputs:
+ group_input_kwargs['Geometry'] = obj_inp.outputs['Geometry']
+ group = nw.new_node(nodegroup.name, input_kwargs=group_input_kwargs)
+
+ geo = group.outputs[k]
+
+ if k.endswith('Curve'):
+ # curves dont export from geonodes well, convert it to a mesh
+ geo = nw.new_node(Nodes.CurveToMesh, [geo])
+
+ output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geo})
+
+ butil.apply_modifiers(vert)
+ bpy.data.node_groups.remove(ng)
+ return vert
\ No newline at end of file
diff --git a/infinigen/assets/utils/laplacian.py b/infinigen/assets/utils/laplacian.py
index 7e3a9a455..ef748eb1f 100644
--- a/infinigen/assets/utils/laplacian.py
+++ b/infinigen/assets/utils/laplacian.py
@@ -4,8 +4,8 @@
# Authors: Lingjie Mei
-import bmesh
import bpy
+import bmesh
import numpy as np
from numpy.random import uniform
from skimage.measure import find_contours, marching_cubes
diff --git a/infinigen/assets/utils/mesh.py b/infinigen/assets/utils/mesh.py
index 8c2a73d35..fd7f1f319 100644
--- a/infinigen/assets/utils/mesh.py
+++ b/infinigen/assets/utils/mesh.py
@@ -1,18 +1,27 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
import bpy
+import bmesh
import numpy as np
+import shapely
+import trimesh
+from mathutils import Vector
from numpy.random import normal, uniform
+from shapely import LineString
-from infinigen.assets.utils.object import new_cube
+from infinigen.assets.utils.decorate import read_co, read_edges, read_edge_length, remove_faces, read_area
+from infinigen.assets.utils.object import new_cube, obj2trimesh, separate_loose
+from infinigen.assets.utils.shapes import dissolve_limited
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
from infinigen.core import surface
from infinigen.core.util import blender as butil
+from infinigen.core.util.math import normalize
def build_prism_mesh(n=6, r_min=1., r_max=1.5, height=.3, tilt=.3):
@@ -24,14 +33,17 @@ def build_prism_mesh(n=6, r_min=1., r_max=1.5, height=.3, tilt=.3):
r_upper = uniform(r_min, r_max, n)
r_lower = uniform(r_min, r_max, n)
- vertices = np.block([[r_upper * np.cos(angles + a_upper), r_lower * np.cos(angles + a_lower), 0, 0],
- [r_upper * np.sin(angles + a_upper), r_lower * np.sin(angles + a_lower), 0, 0],
- [z_upper, -z_lower, 1, -1]]).T
+ vertices = np.block(
+ [[r_upper * np.cos(angles + a_upper), r_lower * np.cos(angles + a_lower), 0, 0],
+ [r_upper * np.sin(angles + a_upper), r_lower * np.sin(angles + a_lower), 0, 0],
+ [z_upper, -z_lower, 1, -1]]
+ ).T
r = np.arange(n)
s = np.roll(r, -1)
faces = np.block(
- [[r, r, r + n, s + n], [s, r + n, s + n, r + n], [np.full(n, 2 * n), s, s, np.full(n, 2 * n + 1)]]).T
+ [[r, r, r + n, s + n], [s, r + n, s + n, r + n], [np.full(n, 2 * n), s, s, np.full(n, 2 * n + 1)]]
+ ).T
mesh = bpy.data.meshes.new('prism')
mesh.from_pydata(vertices, [], faces)
mesh.update()
@@ -45,15 +57,18 @@ def build_convex_mesh(n=6, height=.2, tilt=.2):
z_upper = 1 + normal(0, height, n) + uniform(0, tilt) * np.cos(angles + uniform(-np.pi, np.pi))
z_lower = 1 + normal(0, height, n) + uniform(0, tilt) * np.cos(angles + uniform(-np.pi, np.pi))
r = 1.8
- vertices = np.block([[r * np.cos(angles + a_upper), r * np.cos(angles + a_lower), 0, 0],
- [r * np.sin(angles + a_upper), r * np.sin(angles + a_lower), 0, 0],
- [z_upper, -z_lower, z_upper.max() + uniform(.1, .2),
- -z_lower.max() - uniform(.1, .2)]]).T
+ vertices = np.block(
+ [[r * np.cos(angles + a_upper), r * np.cos(angles + a_lower), 0, 0],
+ [r * np.sin(angles + a_upper), r * np.sin(angles + a_lower), 0, 0],
+ [z_upper, -z_lower, z_upper.max() + uniform(.1, .2),
+ -z_lower.max() - uniform(.1, .2)]]
+ ).T
r = np.arange(n)
s = np.roll(r, -1)
faces = np.block(
- [[r, r, r + n, s + n], [s, r + n, s + n, r + n], [np.full(n, 2 * n), s, s, np.full(n, 2 * n + 1)]]).T
+ [[r, r, r + n, s + n], [s, r + n, s + n, r + n], [np.full(n, 2 * n), s, s, np.full(n, 2 * n + 1)]]
+ ).T
mesh = bpy.data.meshes.new('prism')
mesh.from_pydata(vertices, [], faces)
mesh.update()
@@ -69,3 +84,240 @@ def polygon_angles(n, min_angle=np.pi / 6, max_angle=np.pi * 2 / 3):
else:
angles = np.sort((np.arange(n) * (2 * np.pi / n) + uniform(0, np.pi * 2)) % (np.pi * 2))
return angles
+
+
+def face_area(obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ return sum(f.calc_area() for f in bm.faces)
+
+
+def centroid(obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ s = sum((f.calc_area() * f.calc_center_median() for f in bm.faces), Vector((0, 0, 0)))
+ area = sum(f.calc_area() for f in bm.faces)
+ return np.array(s / area)
+
+
+def longest_ray(obj, obj_, direction):
+ co = read_co(obj_)
+ directions = np.array([direction] * len(co))
+ mesh = obj2trimesh(obj)
+ signed_distance = trimesh.proximity.longest_ray(mesh, co, directions)
+ return signed_distance
+
+
+def treeify(obj):
+ if len(obj.data.vertices) == 0:
+ return obj
+
+ obj = separate_loose(obj)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ included = np.zeros(len(bm.verts))
+ i = min((v.co[-1], i) for i, v in enumerate(bm.verts))[1]
+ queue = [bm.verts[i]]
+ included[i] = 1
+ to_keep = []
+ while queue:
+ v = queue.pop()
+ for e in v.link_edges:
+ o = e.other_vert(v)
+ if not included[o.index]:
+ included[o.index] = 1
+ to_keep.append(e)
+ queue.append(o)
+ bmesh.ops.delete(bm, geom=list(set(bm.edges).difference(to_keep)), context='EDGES')
+ bmesh.update_edit_mesh(obj.data)
+ return obj
+
+
+def convert2ls(obj):
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ verts = [next(v for v in bm.verts if len(v.link_edges) == 1)]
+ for i in range(len(bm.verts) - 1):
+ vs = [e.other_vert(verts[-1]) for e in verts[-1].link_edges]
+ if len(verts) > 1 and len(vs) > 1:
+ verts.append(next(_ for _ in vs if _ != verts[-2]))
+ else:
+ verts.append(vs[0])
+ return LineString(np.array([v.co for v in verts]))
+
+
+def convert2mls(obj):
+ mls = []
+ for o in butil.split_object(obj):
+ mls.append(convert2ls(o))
+ return shapely.MultiLineString(mls)
+
+
+def fix_tree(obj):
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ bpy.ops.mesh.remove_doubles()
+ bm = bmesh.from_edit_mesh(obj.data)
+ vertices_remove = []
+ for v in bm.verts:
+ if len(v.link_edges) == 1:
+ o = v.link_edges[0].other_vert(v)
+ if len(o.link_edges) > 2:
+ vertices_remove.append(v)
+ bmesh.ops.delete(bm, geom=vertices_remove)
+ bmesh.update_edit_mesh(obj.data)
+ return obj
+
+
+def longest_path(obj):
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ bpy.ops.mesh.remove_doubles()
+ bm = bmesh.from_edit_mesh(obj.data)
+
+ def longest_path_(u, v):
+ dist = 0
+ rest = [v]
+ for e in v.link_edges:
+ w = e.other_vert(v)
+ if w != u:
+ l, r = longest_path_(v, w)
+ dist = max(dist, l)
+ rest.extend(r)
+ return dist + np.linalg.norm(u.co - v.co), rest
+
+ while True:
+ for v in bm.verts:
+ if len(v.link_edges) > 2:
+ ws = [e.other_vert(v) for e in v.link_edges]
+ dists, rests = list(zip(*list(longest_path_(v, w) for w in ws)))
+ geom = sum(list(rests[i] for i in np.argsort(dists)[:-2]), [])
+ bmesh.ops.delete(bm, geom=geom)
+ break
+ else:
+ break
+ bmesh.update_edit_mesh(obj.data)
+ return obj
+
+
+def bevel(obj, width, **kwargs):
+ preset = np.random.choice(['LINE', 'SUPPORTS', 'CORNICE', 'CROWN', 'STEPS'])
+ obj, mod = butil.modify_mesh(
+ obj, 'BEVEL', width=width, segments=np.random.randint(20, 30),
+ profile_type='CUSTOM', apply=False, return_mod=True, **kwargs
+ )
+ reset_preset(mod.custom_profile, preset)
+ butil.apply_modifiers(obj, mod)
+
+
+def reset_preset(profile, name, n=None):
+ if n is None:
+ n = np.random.randint(8, 15)
+ match name:
+ case 'LINE':
+ configs = [(1.0, 0.0, 0, 'AUTO', 'AUTO'), (0.0, 1.0, 0, 'AUTO', 'AUTO')]
+ case 'CORNICE':
+ configs = [(1.0, 0.0, 0, 'VECTOR', 'VECTOR'), (1.0, 0.125, 0, 'VECTOR', 'VECTOR'),
+ (0.92, 0.16, 0, 'AUTO', 'AUTO'), (0.875, 0.25, 0, 'VECTOR', 'VECTOR'),
+ (0.8, 0.25, 0, 'VECTOR', 'VECTOR'), (0.733, 0.433, 0, 'AUTO', 'AUTO'),
+ (0.582, 0.522, 0, 'AUTO', 'AUTO'), (0.4, 0.6, 0, 'AUTO', 'AUTO'),
+ (0.289, 0.727, 0, 'AUTO', 'AUTO'), (0.25, 0.925, 0, 'VECTOR', 'VECTOR'),
+ (0.175, 0.925, 0, 'VECTOR', 'VECTOR'), (0.175, 1.0, 0, 'VECTOR', 'VECTOR'),
+ (0.0, 1.0, 0, 'VECTOR', 'VECTOR')]
+ case 'CROWN':
+ configs = [(1.0, 0.0, 0, 'VECTOR', 'VECTOR'), (1.0, 0.25, 0, 'VECTOR', 'VECTOR'),
+ (0.75, 0.25, 0, 'VECTOR', 'VECTOR'), (0.75, 0.325, 0, 'VECTOR', 'VECTOR'),
+ (0.925, 0.4, 0, 'AUTO', 'AUTO'), (0.975, 0.5, 0, 'AUTO', 'AUTO'),
+ (0.94, 0.65, 0, 'AUTO', 'AUTO'), (0.85, 0.75, 0, 'AUTO', 'AUTO'),
+ (0.75, 0.875, 0, 'AUTO', 'AUTO'), (0.7, 1.0, 0, 'VECTOR', 'VECTOR'),
+ (0.0, 1.0, 0, 'VECTOR', 'VECTOR')]
+ case 'SUPPORTS':
+ configs = [(1.0, 0.0, 0, 'VECTOR', 'VECTOR'), (1.0, 0.5, 0, 'VECTOR', 'VECTOR')] + list(
+ (1 - .5 * (
+ 1 - np.cos(i / (n - 3) * np.pi / 2)), .5 + .5 * np.sin(i / (n - 3) * np.pi / 2), 0, 'AUTO',
+ 'AUTO') for i in range(1, n - 2)
+ ) + [(0.5, 1.0, 0, 'VECTOR', 'VECTOR'),
+ (0.0, 1.0, 0, 'VECTOR', 'VECTOR')]
+ case _:
+ n_steps_x = n if n % 2 == 0 else n - 1
+ n_steps_y = n - 2 if n % 2 == 0 else n - 1
+ configs = list(
+ (1 - (i + 1) // 2 * 2 / n_steps_x, i // 2 * 2 / n_steps_y, 0, 'VECTOR', 'VECTOR') for i in
+ range(n)
+ )
+ k = len(configs) - len(profile.points)
+ for i in range(k):
+ profile.points.add((i + 1) / (k + 1), 0)
+ for p, c in zip(profile.points, configs):
+ p.location = c[0], c[1]
+ p.select = True
+ p.handle_type_1 = c[3]
+ p.handle_type_2 = c[4]
+ p.select = False
+ profile.points.update()
+
+
+def canonicalize_ls(line):
+ line = shapely.simplify(line, .02)
+ while True:
+ coords = np.array(line.coords)
+ diff = coords[1:] - coords[:-1]
+ diff = diff / (np.linalg.norm(diff, axis=-1, keepdims=True) + 1e-6)
+ product = (diff[:-1] * diff[1:]).sum(-1)
+ valid_indices = (np.nonzero((1 - 1e-6 > product) & (product > -.8))[0] + 1).tolist()
+ ls = LineString(coords[[0] + valid_indices + [-1]])
+ if ls.length < line.length:
+ line = ls
+ else:
+ break
+ return ls
+
+
+def canonicalize_mls(mls):
+ return shapely.MultiLineString([canonicalize_ls(ls) for ls in mls.geoms])
+
+
+def separate_selected(obj, face=False):
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ if face:
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+ o = next(o for o in bpy.context.selected_objects if o != obj)
+ butil.select_none()
+ return o
+
+
+def snap_mesh(obj, eps=1e-3):
+ while True:
+ dissolve_limited(obj)
+ co = read_co(obj)
+ u, w = read_edges(obj).T
+ d = co[:, np.newaxis] - co[np.newaxis, u]
+ l = co[np.newaxis, w] - co[np.newaxis, u]
+ n = normalize(l, in_place=False)
+ prod = (d * n).sum(-1)
+ diff = np.linalg.norm(d - prod[:, :, np.newaxis] * n, axis=-1)
+ diff[u, np.arange(len(u))] = 1
+ diff[w, np.arange(len(w))] = 1
+ diff[prod < 0] = 1
+ diff[prod > np.linalg.norm(l, axis=-1)] = 1
+ es, vs = np.nonzero((diff < eps).T)
+ if len(vs) == 0:
+ return obj
+ indices = np.concatenate([[0], np.nonzero(es[1:] != es[:-1])[0] + 1])
+ vs = vs[indices]
+ es = es[indices]
+ with butil.ViewportMode(obj, 'EDIT'):
+ bm = bmesh.from_edit_mesh(obj.data)
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ dis = co[w[es]] - co[u[es]]
+ norms = np.linalg.norm(dis, axis=-1)
+ percents = ((co[vs] - co[u[es]]) * dis).sum(-1) / (norms ** 2)
+ edges = [bm.edges[e] for e in es]
+ for e, p in zip(edges, percents):
+ bmesh.ops.subdivide_edges(bm, edges=[e], cuts=1, edge_percents={e: p})
+ bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=eps * 1.5)
+ bmesh.update_edit_mesh(obj.data)
+
diff --git a/infinigen/assets/utils/misc.py b/infinigen/assets/utils/misc.py
index da5753db3..006a54043 100644
--- a/infinigen/assets/utils/misc.py
+++ b/infinigen/assets/utils/misc.py
@@ -1,5 +1,8 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+import string
+from functools import update_wrapper, wraps
# Authors: Lingjie Mei
@@ -8,8 +11,11 @@
import numpy as np
from numpy.random import normal, uniform
-from infinigen.core.nodes.node_info import Nodes
-from infinigen.core.nodes.node_wrangler import NodeWrangler
+from infinigen.assets.utils.object import origin2lowest
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import clip_gaussian
+from infinigen.core.util.random import log_uniform # imported by other files
+from infinigen.core.nodes import NodeWrangler, Nodes
class CountInstance:
@@ -30,10 +36,6 @@ def __exit__(self, *args):
print(f"{count - self.count} {self.name} instances created.")
-def log_uniform(low, high, size=None):
- return np.exp(uniform(np.log(low), np.log(high), size))
-
-
def sample_direction(min_z):
for _ in range(100):
x = normal(size=3)
@@ -43,6 +45,27 @@ def sample_direction(min_z):
return 0, 0, 1
+def subclasses(cls):
+ return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in subclasses(c)])
+
+
+def make_normalized_factory(cls):
+ @wraps(cls, updated=())
+ class CLS(cls):
+ def __init__(self, *args, **kwargs):
+ super(CLS, self).__init__(*args, **kwargs)
+ update_wrapper(self, *args, **kwargs)
+
+ def create_asset(self, **params):
+ obj = super(CLS, self).create_asset(**params)
+ obj.rotation_euler = uniform(-np.pi, np.pi, 3)
+ butil.apply_transform(obj)
+ origin2lowest(obj)
+ return obj
+
+ return CLS
+
+
def build_color_ramp(nw: NodeWrangler, x, positions, colors, mode='HSV'):
cr = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': x})
cr.color_ramp.color_mode = mode
@@ -64,3 +87,56 @@ def make_circular_angle(xs):
def make_circular(xs):
return np.array([xs[-1], *xs, xs[0]])
+
+
+def toggle_hide(obj, recursive=True):
+ if obj.name in bpy.data.collections:
+ obj.hide_viewport = True
+ obj.hide_render = True
+ for o in obj.objects:
+ toggle_hide(o, recursive)
+ else:
+ obj.hide_set(True)
+ obj.hide_render = True
+ if recursive:
+ for c in obj.children:
+ toggle_hide(c)
+
+
+def toggle_show(obj, recursive=True):
+ if obj.name in bpy.data.collections:
+ obj.hide_viewport = False
+ obj.hide_render = False
+ for o in obj.objects:
+ toggle_show(o, recursive)
+ else:
+ obj.hide_set(False)
+ obj.hide_render = False
+ if recursive:
+ for c in obj.children:
+ toggle_hide(c)
+
+
+def assign_material(obj, material):
+ if not isinstance(obj, list):
+ obj = [obj]
+ for o in obj:
+ with butil.SelectObjects(o):
+ while len(o.data.materials):
+ bpy.ops.object.material_slot_remove()
+ if not isinstance(material, list):
+ material = [material]
+ for m in material:
+ o.data.materials.append(m)
+
+
+character_set = list(string.ascii_lowercase + string.ascii_uppercase + string.digits)
+character_set_weights = np.concatenate(
+ [1.5 * np.ones(len(string.ascii_lowercase)), 0.5 * np.ones(len(string.ascii_uppercase)),
+ 0.5 * np.ones(len(string.digits))])
+character_set_weights /= character_set_weights.sum()
+
+
+def generate_text():
+ return "".join(np.random.choice(character_set, size=int(clip_gaussian(3, 7, 2, 15)), replace=True,
+ p=character_set_weights))
diff --git a/infinigen/assets/utils/nodegroup.py b/infinigen/assets/utils/nodegroup.py
index c130ed24f..5f559825d 100644
--- a/infinigen/assets/utils/nodegroup.py
+++ b/infinigen/assets/utils/nodegroup.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
@@ -9,7 +10,7 @@
import bpy
import numpy as np
-from infinigen.assets.utils.decorate import toggle_hide
+from infinigen.assets.utils.misc import toggle_hide
from infinigen.core.nodes import node_utils
from infinigen.core.nodes.node_info import Nodes
from infinigen.core.nodes.node_wrangler import NodeWrangler
@@ -42,12 +43,17 @@ def build_curve(nw: NodeWrangler, positions, circular=False, handle='VECTOR'):
return curve
-def geo_radius(nw: NodeWrangler, radius, resolution=6, merge_distance=.004):
+def geo_radius(nw: NodeWrangler, radius, resolution=6, merge_distance=.004, rotation=0, to_align_tilt=True,
+ align_tilt_axis=(0, 0, 1)):
skeleton = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
radius = surface.eval_argument(nw, radius)
- curve = align_tilt(nw, nw.new_node(Nodes.MeshToCurve, [skeleton]))
+ curve = nw.new_node(Nodes.MeshToCurve, [skeleton])
+ if to_align_tilt:
+ curve = align_tilt(nw, curve, align_tilt_axis)
skeleton = nw.new_node(Nodes.SetCurveRadius, input_kwargs={'Curve': curve, 'Radius': radius})
- geometry = nw.curve2mesh(skeleton, nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': resolution}))
+ geometry = nw.curve2mesh(skeleton, nw.new_node(Nodes.Transform, [
+ nw.new_node(Nodes.CurveCircle, input_kwargs={'Resolution': resolution})],
+ input_kwargs={'Rotation': [0, 0, rotation]}))
if merge_distance > 0:
geometry = nw.new_node(Nodes.MergeByDistance, [geometry, None, merge_distance])
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
@@ -60,6 +66,14 @@ def geo_selection(nw: NodeWrangler, selection):
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+def geo_selection_attribute(nw: NodeWrangler, selection, name, domain='POINT'):
+ geometry = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ selection = surface.eval_argument(nw, selection)
+ geometry = nw.new_node(Nodes.StoreNamedAttribute, [geometry, None, name, None, selection],
+ attrs={'domain': domain})
+ nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry})
+
+
def geo_base_selection(nw: NodeWrangler, base_obj, selection, merge_threshold=0):
geometry = nw.new_node(Nodes.ObjectInfo, [base_obj], attrs={'transform_space': 'RELATIVE'}).outputs[
'Geometry']
diff --git a/infinigen/assets/utils/object.py b/infinigen/assets/utils/object.py
index a60363093..f8ebe7e9a 100644
--- a/infinigen/assets/utils/object.py
+++ b/infinigen/assets/utils/object.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Lingjie Mei
@@ -11,18 +12,27 @@
import infinigen.core.util.blender as butil
from infinigen.assets.utils.decorate import read_co
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import select_none
def center(obj):
return (Vector(obj.bound_box[0]) + Vector(obj.bound_box[-2])) * obj.scale / 2.
-def origin2lowest(obj, vertical=False):
+def origin2lowest(obj, vertical=False, centered=False, approximate=False):
co = read_co(obj)
if not len(co):
return
i = np.argmin(co[:, -1])
- if vertical:
+ if approximate:
+ indices = np.argsort(co[:, -1])
+ obj.location = -np.mean(co[indices[:len(co) // 10]], 0)
+ obj.location[-1] = -co[i, -1]
+ elif centered:
+ obj.location = -center(obj)
+ obj.location[-1] = -co[i, -1]
+ elif vertical:
obj.location[-1] = -co[i, -1]
else:
obj.location = -co[i]
@@ -65,9 +75,7 @@ def trimesh2obj(trimesh):
def obj2trimesh(obj):
- with butil.ViewportMode(obj, 'EDIT'):
- bpy.ops.mesh.select_all(action='SELECT')
- bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ butil.modify_mesh(obj, 'TRIANGULATE', min_vertices=3)
vertices = read_co(obj)
arr = np.zeros(len(obj.data.polygons) * 3)
obj.data.polygons.foreach_get('vertices', arr)
@@ -81,6 +89,22 @@ def new_cube(**kwargs):
return bpy.context.active_object
+def new_bbox(x, x_, y, y_, z, z_):
+ obj = new_cube()
+ obj.location = (x + x_) / 2, (y + y_) / 2, (z + z_) / 2
+ obj.scale = (x_ - x) / 2, (y_ - y) / 2, (z_ - z) / 2
+ butil.apply_transform(obj, True)
+ return obj
+
+
+def new_bbox_2d(x, x_, y, y_, z=0):
+ obj = new_plane()
+ obj.location = (x + x_) / 2, (y + y_) / 2, z
+ obj.scale = (x_ - x) / 2, (y_ - y) / 2, 1
+ butil.apply_transform(obj, True)
+ return obj
+
+
def new_icosphere(**kwargs):
kwargs['location'] = kwargs.get('location', (0, 0, 0))
bpy.ops.mesh.primitive_ico_sphere_add(**kwargs)
@@ -95,6 +119,13 @@ def new_circle(**kwargs):
return obj
+def new_base_circle(**kwargs):
+ kwargs['location'] = kwargs.get('location', (0, 0, 0))
+ bpy.ops.mesh.primitive_circle_add(**kwargs)
+ obj = bpy.context.active_object
+ return obj
+
+
def new_empty(**kwargs):
kwargs['location'] = kwargs.get('location', (0, 0, 0))
bpy.ops.object.empty_add(**kwargs)
@@ -103,8 +134,81 @@ def new_empty(**kwargs):
return obj
-def new_line(scale=1., subdivisions=7):
- obj = mesh2obj(data2mesh([[0, 0, 0], [scale, 0, 0]], [[0, 1]]))
- butil.modify_mesh(obj, 'SUBSURF', levels=subdivisions, render_levels=subdivisions)
+def new_plane(**kwargs):
+ kwargs['location'] = kwargs.get('location', (0, 0, 0))
+ bpy.ops.mesh.primitive_plane_add(**kwargs)
+ obj = bpy.context.active_object
+ butil.apply_transform(obj, loc=True)
+ return obj
+
+
+def new_cylinder(**kwargs):
+ kwargs['location'] = kwargs.get('location', (0, 0, .5))
+ kwargs['depth'] = kwargs.get('depth', 1)
+ bpy.ops.mesh.primitive_cylinder_add(**kwargs)
+ obj = bpy.context.active_object
+ butil.apply_transform(obj, loc=True)
+ return obj
+
+
+def new_base_cylinder(**kwargs):
+ bpy.ops.mesh.primitive_cylinder_add(**kwargs)
+ obj = bpy.context.active_object
+ butil.apply_transform(obj, loc=True)
+ return obj
+
+
+def new_grid(**kwargs):
+ kwargs['location'] = kwargs.get('location', (0, 0, 0))
+ bpy.ops.mesh.primitive_grid_add(**kwargs)
+ obj = bpy.context.active_object
+ butil.apply_transform(obj, loc=True)
+ return obj
+
+
+def new_line(subdivisions=1, scale=1.):
+ vertices = np.stack(
+ [np.linspace(0, scale, subdivisions + 1), np.zeros(subdivisions + 1), np.zeros(subdivisions + 1)], -1)
+ edges = np.stack([np.arange(subdivisions), np.arange(1, subdivisions + 1)], -1)
+ obj = mesh2obj(data2mesh(vertices, edges))
+ return obj
+
+
+def join_objects(obj):
+ butil.select_none()
+ if not isinstance(obj, list):
+ obj = [obj]
+ if len(obj) == 1:
+ return obj[0]
+ bpy.context.view_layer.objects.active = obj[0]
+ butil.select_none()
+ butil.select(obj)
+ bpy.ops.object.join()
+ obj = bpy.context.active_object
obj.location = 0, 0, 0
+ obj.rotation_euler = 0, 0, 0
+ obj.scale = 1, 1, 1
+ butil.select_none()
+ return obj
+
+
+def separate_loose(obj):
+ select_none()
+ objs = butil.split_object(obj)
+ i = np.argmax([len(o.data.vertices) for o in objs])
+ obj = objs[i]
+ objs.remove(obj)
+ butil.delete(objs)
return obj
+
+
+def print3d_clean_up(obj):
+ bpy.ops.preferences.addon_enable(module='object_print3d_utils')
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ bpy.ops.mesh.fill_holes()
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ bpy.ops.mesh.normals_make_consistent()
+ bpy.ops.mesh.print3d_clean_distorted()
+ bpy.ops.mesh.print3d_clean_non_manifold()
diff --git a/infinigen/assets/utils/reaction_diffusion.py b/infinigen/assets/utils/reaction_diffusion.py
index f1165ccb2..46d7eb516 100644
--- a/infinigen/assets/utils/reaction_diffusion.py
+++ b/infinigen/assets/utils/reaction_diffusion.py
@@ -5,7 +5,7 @@
import math
-
+import bpy
import bmesh
import numpy as np
import tqdm
diff --git a/infinigen/assets/utils/shapes.py b/infinigen/assets/utils/shapes.py
new file mode 100644
index 000000000..82676b1eb
--- /dev/null
+++ b/infinigen/assets/utils/shapes.py
@@ -0,0 +1,142 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+import shapely
+from shapely import Polygon, remove_repeated_points, simplify
+
+from shapely.ops import linemerge, orient, polygonize, unary_union, shared_paths
+from trimesh.creation import triangulate_polygon
+
+from infinigen.assets.utils.decorate import write_co, read_co, select_faces, read_normal
+from infinigen.assets.utils.object import new_circle, data2mesh, mesh2obj, join_objects
+from infinigen.core.util import blender as butil
+
+
+def is_valid_polygon(p):
+ if isinstance(p, Polygon) and p.area > 0 and p.is_valid:
+ if len(p.interiors) == 0:
+ return True
+ return False
+
+
+def simplify_polygon(p):
+ with np.errstate(invalid="ignore"):
+ p = remove_repeated_points(simplify(p, 1e-6).normalize(), .01)
+ return p
+
+
+def cut_polygon_by_line(polygon, *args):
+ merged = linemerge([polygon.boundary, *args])
+ borders = unary_union(merged)
+ polygons = polygonize(borders)
+ return list(polygons)
+
+
+def safe_polygon2obj(p, reversed=False, z=0):
+ ps = [p] if p.geom_type == 'Polygon' else p.geoms
+ objs_ = []
+ for p in ps:
+ p = orient(p).segmentize(.005)
+ try:
+ obj = triangulate_polygon2obj(p)
+ objs_.append(obj)
+ except:
+ try:
+ obj = polygon2obj(p)
+ objs_.append(obj)
+ except:
+ pass
+ if len(objs_) == 0:
+ return None
+ obj = join_objects(objs_)
+ obj.location[-1] = z
+ butil.apply_transform(obj, True)
+ point_normal_up(obj, reversed)
+ return obj
+
+
+def polygon2obj(p, reversed=False, z=0):
+ p = orient(p)
+ coords = np.array(p.exterior.coords)[:-1, :2]
+ obj = new_circle(vertices=len(coords))
+ write_co(obj, np.concatenate([coords, np.zeros((len(coords), 1))], -1))
+ objs = [obj]
+ for i in p.interiors:
+ coords = np.array(i.coords)[:-1, :2]
+ o = new_circle(vertices=len(coords))
+ write_co(o, np.concatenate([coords, np.zeros((len(coords), 1))], -1))
+ objs.append(o)
+ obj = join_objects(objs)
+ butil.modify_mesh(obj, 'WELD', merge_threshold=1e-6)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.fill()
+ dissolve_limited(obj)
+ obj.location[-1] = z
+ butil.apply_transform(obj, True)
+ point_normal_up(obj, reversed)
+ return obj
+
+
+def point_normal_up(obj, reversed=False):
+ with butil.ViewportMode(obj, 'EDIT'):
+ no_z = read_normal(obj)[:, -1]
+ select_faces(obj, (no_z > 0) if reversed else (no_z < 0))
+ bpy.ops.mesh.flip_normals()
+
+
+def triangulate_polygon2obj(p):
+ vertices, faces = triangulate_polygon(orient(p))
+ vertices = np.concatenate([vertices, np.zeros((len(vertices), 1))], -1)
+ obj = mesh2obj(data2mesh(vertices=vertices, faces=faces))
+ co = read_co(obj)
+ co[:, -1] = 0
+ write_co(obj, co)
+ butil.modify_mesh(obj, 'WELD', merge_threshold=1e-6)
+ dissolve_limited(obj)
+ return obj
+
+
+def dissolve_limited(obj):
+ with butil.ViewportMode(obj, 'EDIT'), butil.Suppress():
+ for angle_limit in reversed(.05 * .1 ** np.arange(5)):
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.dissolve_limited(angle_limit=angle_limit)
+
+
+def obj2polygon(obj):
+ co = read_co(obj)[:, :2]
+ p = shapely.union_all(
+ [shapely.make_valid(orient(shapely.Polygon(co[p.vertices]))) for p in obj.data.polygons]
+ )
+ return shapely.make_valid(shapely.simplify(p, 1e-6))
+
+
+def buffer(p, distance):
+ with np.errstate(invalid="ignore"):
+ return remove_repeated_points(simplify(p.buffer(distance, join_style='mitre', cap_style='flat'), 1e-6))
+
+
+def segment_filter(mls, margin):
+ for ls in mls.geoms if mls.geom_type == 'MultiLineString' else [mls]:
+ coords = np.array(ls.coords)
+ if len(coords) < 2:
+ continue
+ elif np.any(np.linalg.norm(coords[1:] - coords[:-1], axis=-1) > margin):
+ return True
+ return False
+
+
+def shared(s, t):
+ with np.errstate(invalid="ignore"):
+ forward, backward = shared_paths(s.boundary, t.boundary).geoms
+ if forward.length > 0:
+ return forward
+ elif backward.length > 0:
+ return backward
+ else:
+ return shapely.MultiLineString()
diff --git a/infinigen/assets/utils/tag.py b/infinigen/assets/utils/tag.py
deleted file mode 100644
index 03901d8f1..000000000
--- a/infinigen/assets/utils/tag.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-
-# Authors: Yihan Wang
-
-
-import os
-import bpy
-import json
-import numpy as np
-import infinigen.core.util.blender as butil
-from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
-
-class AutoTag():
- tag_dict = {}
- def __init__(self):
- self.tag_dict = {}
-
- def rephrase(self, TagName):
- assert len(TagName) > 0, 'TagName is empty'
- tags = TagName.split('.')
- tags.sort()
- name = tags[0]
- for i in range(1, len(tags)):
- name = name + '.' + tags[i]
- return name
-
- def trigger_update(self):
- bpy.ops.object.mode_set(mode='EDIT')
- bpy.ops.object.mode_set(mode='OBJECT')
-
- def get_all_objs(self, obj):
- objs = [obj]
- for obj_child in obj.children:
- objs += self.get_all_objs(obj_child)
- return objs
-
- def add_attribute(self, obj_name, attr_name, type='FLOAT', domain='POINT', value=1.0, recursive=True):
- root_obj = bpy.data.objects[obj_name]
- # print(domain, attr_name, obj_name, type)
- if recursive == False:
- obj = root_obj
- if obj.type != 'MESH':
- attr = None
- else:
- attr = obj.data.attributes.new(attr_name, type, domain)
- val = [value] * len(attr.data)
- attr.data.foreach_set('value', val)
- else:
- objs = self.get_all_objs(root_obj)
- for obj in objs:
- if obj.type != 'MESH':
- attr = None
- else:
- attr = obj.data.attributes.new(attr_name, type, domain)
- val = [value] * len(attr.data)
- attr.data.foreach_set('value', val)
- return attr
-
-
- # This function now only supports APPLIED OBJECTS
- # PLEASE KEEP ALL THE GEOMETRY APPLIED BEFORE SCATTERING THEM ON THE TERRAIN
- # PLEASE DO NOT USE BOOLEAN TAGS FOR OTHER USE
- def save_tag(self, path='./MaskTag.json'):
- with open(path, 'w') as f:
- json.dump(self.tag_dict, f)
-
- def load_tag(self, path='./MaskTag.json'):
- with open(path, 'r') as f:
- self.tag_dict = json.load(f)
-
- def relabel_obj(self, root_obj):
-
- tag_dict = self.tag_dict
- tag_name = [0] * len(tag_dict)
- for name, tag_id in tag_dict.items():
- tag_name[int(tag_id) - 1] = name
-
- objs = self.get_all_objs(root_obj)
- for obj in objs:
- if obj.type != 'MESH':
- continue
-
- attr_dict = {}
- n = 0
- tag = None
- for name, attr in obj.data.attributes.items():
- if 'TAG_' in name:
- n = len(attr.data)
- val = n * [0]
- attr.data.foreach_get('value', val)
- attr_dict[name[4:]] = val
-
- if name == 'MaskTag':
- n = len(attr.data)
- tag = n * [0]
- attr.data.foreach_get('value', tag)
-
- for name in attr_dict.keys():
- obj.data.attributes.remove(obj.data.attributes['TAG_' + name])
-
- assert (len(attr_dict) > 0) or (tag is not None), 'No tag for object {}'.format(obj.name)
-
- MaskTag = [0] * n
- for i in range(n):
- TagName = None
- if (tag is not None) and (tag[i] > 0):
- TagName = tag_name[tag[i] - 1]
- for name, val in attr_dict.items():
- if val[i] == True:
- if TagName is None:
- TagName = name
- else:
- TagName = TagName + '.' + name
- TagName = self.rephrase(TagName)
- TagValue = tag_dict.get(TagName, -1)
- if TagValue == -1:
- TagValue = len(tag_dict) + 1
- tag_dict[TagName] = TagValue
- tag_name.append(TagName)
- MaskTag[i] = TagValue
-
- if tag is None:
- MaskTag_attr = self.add_attribute(obj.name, 'MaskTag', type='INT', value=1, recursive=False)
- else:
- MaskTag_attr = obj.data.attributes['MaskTag']
-
- MaskTag_attr.data.foreach_set('value', MaskTag)
- self.tag_dict = tag_dict
-
- return root_obj
-
-
-tag_system = AutoTag()
-
-def tag_object(obj, name=""):
- if name != "":
- name = 'TAG_' + name
- tag_system.add_attribute(obj.name, name, type='BOOLEAN', value=True)
- tag_system.relabel_obj(obj)
-
-
-def tag_nodegroup(nw, input_node, name):
- name = 'TAG_' + name
- store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute,
- input_kwargs={'Geometry': input_node, 'Name': name, 'Value': True},
- attrs={'domain': 'POINT', 'data_type': 'BOOLEAN'})
- return store_named_attribute
-
\ No newline at end of file
diff --git a/infinigen/assets/utils/uv.py b/infinigen/assets/utils/uv.py
new file mode 100644
index 000000000..72e8090a6
--- /dev/null
+++ b/infinigen/assets/utils/uv.py
@@ -0,0 +1,201 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections.abc import Iterable
+
+import bpy
+import bmesh
+import numpy as np
+from sklearn.linear_model import LinearRegression
+
+from infinigen.assets.materials import common
+from infinigen.assets.utils.decorate import (
+ read_co, read_edges, read_loop_edges, read_loop_starts,
+ read_loop_totals, read_loop_vertices, read_normal, read_uv, select_faces, write_uv,
+)
+from infinigen.core.util import blender as butil
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def face_corner2faces(obj):
+ loop_starts = read_loop_starts(obj)
+ faces = np.zeros(len(obj.data.loops), dtype=int)
+ faces[loop_starts] = 1
+ faces = np.cumsum(faces) - 1
+ return faces
+
+
+def unwrap_faces(obj, selection=None):
+ if isinstance(obj, Iterable):
+ for o in obj:
+ unwrap_faces(o, selection)
+ return
+ butil.select_none()
+ selection = common.get_selection(obj, selection)
+ if len(obj.data.uv_layers) == 0:
+ smart = True
+ else:
+ uv = read_uv(obj)[selection.astype(bool)[face_corner2faces(obj)]]
+ smart = (np.isnan(uv) | (np.abs(uv) < .1)).sum() / uv.size > .5
+ butil.select_none()
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(type="FACE")
+ select_faces(obj, selection)
+ if smart:
+ bpy.ops.uv.smart_project()
+ else:
+ bpy.ops.uv.unwrap()
+
+
+def str2vec(axis):
+ if not isinstance(axis, str):
+ return axis
+ match axis[-1].lower():
+ case 'x':
+ vec = 1, 0, 0
+ case 'y':
+ vec = 0, 1, 0
+ case 'z':
+ vec = 0, 0, 1
+ case 'u':
+ vec = -1, 0, 0
+ case 'v':
+ vec = 0, -1, 0
+ case 'w':
+ vec = 0, 0, -1
+ case _:
+ raise NotImplementedError
+ vec = np.array(vec)
+ if axis[0] == '-':
+ vec = -vec
+ return vec
+
+
+def compute_uv_direction(obj, x='x', y='y', selection=None):
+ ensure_uv(obj, selection)
+ x, y = str2vec(x), str2vec(y)
+ co = read_co(obj)
+ edges = read_edges(obj)
+ loop_vertices = read_loop_vertices(obj)
+ loop_edges = read_loop_edges(obj)
+ uv = read_uv(obj)
+ if selection is None:
+ selection = np.full(len(uv), True)
+ selection = selection.astype(bool)
+ loop_starts = read_loop_starts(obj)
+ loop_totals = read_loop_totals(obj)
+ next_vertices = edges[loop_edges].sum(1) - loop_vertices
+ next_loops = np.arange(len(uv)) + 1
+ next_loops[loop_starts + loop_totals - 1] -= loop_totals
+ uv_diff = uv[next_loops] - uv
+ co_diff = co[next_vertices] - co[loop_vertices]
+ lr = LinearRegression()
+ lr.fit(co_diff[selection], uv_diff[selection])
+ lr.coef_[lr.coef_ > 1e3] = 0
+ axes = lr.predict(np.stack([x, y]))
+ axes = axes / (np.linalg.norm(axes, axis=-1) + 1e-6)
+ pred = uv @ axes.T
+ pred_sel = pred[selection]
+ x_min, x_max = np.min(pred_sel[:, 0]), np.max(pred_sel[:, 0])
+ y_min, y_max = np.min(pred_sel[:, 1]), np.max(pred_sel[:, 1])
+ if x_max - x_min > y_max - y_min:
+ scale = 1 / (x_max - x_min + 1e-4)
+ mid = (y_max + y_min) / 2
+ pred = np.stack([(pred[:, 0] - x_min) * scale, (pred[:, 1] - mid) * scale + .5], -1)
+ bbox = 0, 1, .5 - .5 * (y_max - y_min) * scale, .5 + .5 * (y_max - y_min) * scale
+ else:
+ scale = 1 / (y_max - y_min + 1e-4)
+ mid = (x_max + x_min) / 2
+ pred = np.stack([(pred[:, 0] - mid) * scale + .5, (pred[:, 1] - y_min) * scale], -1)
+ bbox = .5 - .5 * (x_max - x_min) * scale, .5 + .5 * (x_max - x_min) * scale, 0, 1
+ new_uv = np.where(selection[:, np.newaxis], pred, uv)
+ write_uv(obj, new_uv)
+ return bbox
+
+
+def max_bbox(bboxes):
+ return min(b[0] for b in bboxes), max(b[1] for b in bboxes), min(b[2] for b in bboxes), max(
+ b[3] for b in bboxes
+ )
+
+
+def wrap_sides(obj, surface, axes, xs, ys, groupings=None, selection=None, **kwargs):
+ fc2f = face_corner2faces(obj)
+ axes = np.array([str2vec(axis) for axis in axes])
+ faces = np.argmax(read_normal(obj) @ axes.T, -1)
+ selection = common.get_selection(obj, selection)
+ faces = np.where(selection, faces, -1)
+ bboxes, selections = [], []
+ for i in range(len(axes)):
+ selected = faces == i
+ selections.append(selected)
+ unwrap_faces(obj, selected)
+ bboxes.append(compute_uv_direction(obj, str2vec(xs[i]), str2vec(ys[i]), selected[fc2f]))
+ if groupings is None:
+ groupings = [[i] for i in range(len(axes))]
+ for indices in groupings:
+ selected = sum(selections[i] for i in indices)
+ try:
+ surface.apply(obj, selected, bbox=max_bbox([bboxes[i] for i in indices]), **kwargs)
+ except TypeError:
+ logger.debug(f'apply() for {surface=} with kwarg bbox failed, trying again without')
+ surface.apply(obj, selected, **kwargs)
+
+
+def wrap_front_back(obj, surface, shared=True, **kwargs):
+ wrap_sides(obj, surface, 'vy', 'xu', 'zz', [[0, 1]] if shared else None, **kwargs)
+
+def wrap_top_bottom(obj, surface, shared=True, **kwargs):
+ wrap_sides(obj, surface, 'zw', 'xu', 'yy', [[0, 1]] if shared else None, **kwargs)
+
+
+def wrap_front_back_side(obj, surface, shared=True, **kwargs):
+ wrap_sides(obj, surface, 'vuy', 'xyu', 'zzz', [[0, 2], [1]] if shared else None, **kwargs)
+
+
+def wrap_four_sides(obj, surface, shared=True, **kwargs):
+ wrap_sides(obj, surface, 'vxyu', 'xyuv', 'zzzz', [[0, 2], [1, 3]] if shared else None, **kwargs)
+
+
+def wrap_six_sides(obj, surface, shared=True, **kwargs):
+ wrap_sides(
+ obj, surface, 'vxyuzw', 'xyuvxx', 'zzzzyv', [[0, 2], [1, 3], [4, 5]] if shared else None,
+ **kwargs
+ )
+
+
+def unwrap_normal(obj, selection=None, axis=None, axis_=None):
+ ensure_uv(obj)
+ normal = read_normal(obj)
+ loop_vertices = read_loop_vertices(obj)
+ co = read_co(obj)
+ loop_totals = read_loop_totals(obj)
+ normal = normal[np.arange(len(obj.data.polygons)).repeat(loop_totals)]
+ selection = common.get_selection(obj, selection).repeat(loop_totals)
+ if axis is not None:
+ axis = str2vec(axis)
+ axis_ = np.cross(normal, axis)
+ axis = axis[np.newaxis, :]
+ elif axis_ is not None:
+ axis_ = str2vec(axis_)
+ axis = np.cross(normal, axis_)
+ axis_ = axis_[np.newaxis, :]
+ else:
+ axis = np.zeros(3)
+ i = np.argmin(np.abs(normal)[selection.astype(bool)].sum(0))
+ axis[i] = 1
+ axis = axis[np.newaxis, :] - np.inner(axis, normal)[:, np.newaxis] * normal
+ axis /= np.maximum(np.linalg.norm(axis, axis=-1, keepdims=True), 1e-4)
+ axis_ = np.cross(normal, axis)
+ uv = np.stack([(co[loop_vertices] * axis).sum(1), (co[loop_vertices] * axis_).sum(1)], -1)
+ uv = np.where(selection[:, np.newaxis], uv, read_uv(obj))
+ write_uv(obj, uv)
+
+
+def ensure_uv(obj, selection=None):
+ if len(obj.data.uv_layers) == 0:
+ unwrap_faces(obj, selection)
diff --git a/infinigen/assets/wall_decorations/__init__.py b/infinigen/assets/wall_decorations/__init__.py
new file mode 100644
index 000000000..e437b4f72
--- /dev/null
+++ b/infinigen/assets/wall_decorations/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .balloon import BalloonFactory
+from .wall_art import WallArtFactory, MirrorFactory
+from .wall_shelf import WallShelfFactory
+from .range_hood import RangeHoodFactory
diff --git a/infinigen/assets/wall_decorations/balloon.py b/infinigen/assets/wall_decorations/balloon.py
new file mode 100644
index 000000000..bb8cc032f
--- /dev/null
+++ b/infinigen/assets/wall_decorations/balloon.py
@@ -0,0 +1,78 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.scatters import clothes
+from infinigen.assets.utils.decorate import subdivide_edge_ring, subsurf
+from infinigen.assets.utils.draw import remesh_fill
+from infinigen.assets.utils.misc import generate_text
+from infinigen.assets.utils.object import new_bbox
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed
+from infinigen.assets.material_assignments import AssetList
+
+
+class BalloonFactory(AssetFactory):
+ alpha = .8
+
+ def __init__(self, factory_seed, coarse=False):
+ super(BalloonFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.thickness = uniform(.06, .1)
+ material_assignments = AssetList['BalloonFactory']()
+ self.surface = material_assignments['surface'].assign_material()
+ self.rel_scale = uniform(.2, .3) * 4
+ self.displace = uniform(.02, .04)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ bpy.ops.object.text_add()
+ obj = bpy.context.active_object
+
+ with butil.ViewportMode(obj, 'EDIT'):
+ for _ in 'Text':
+ bpy.ops.font.delete(type='PREVIOUS_OR_SELECTION')
+ text = generate_text().upper()
+ bpy.ops.font.text_insert(text=text)
+ with butil.SelectObjects(obj):
+ bpy.ops.object.convert(target='MESH')
+ obj = bpy.context.active_object
+ parent = new_bbox(
+ -self.thickness / 2, self.thickness / 2, 0, self.rel_scale * len(text) * self.alpha,
+ 0, self.rel_scale * self.alpha
+ )
+ obj.parent = parent
+ return parent
+
+ def create_asset(self, i, placeholder, **params) -> bpy.types.Object:
+ obj = placeholder.children[0]
+ obj.parent = None
+ remesh_fill(obj, .02)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=self.thickness, offset=.5)
+ subdivide_edge_ring(obj, 8, (0, 0, 1))
+
+ clothes.cloth_sim(
+ obj,
+ tension_stiffness=uniform(0, 5),
+ gravity=0,
+ use_pressure=True,
+ uniform_pressure_force=uniform(10, 20),
+ vertex_group_mass='pin'
+ )
+
+ subsurf(obj, 1)
+ obj.scale = [self.rel_scale] * 3
+ obj.rotation_euler = np.pi / 2, 0, np.pi / 2
+ butil.apply_transform(obj, True)
+ butil.modify_mesh(obj, 'DISPLACE', strength=self.displace)
+ butil.modify_mesh(obj, 'SMOOTH', iterations=5)
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
diff --git a/infinigen/assets/wall_decorations/range_hood.py b/infinigen/assets/wall_decorations/range_hood.py
new file mode 100644
index 000000000..2cbbfd7b8
--- /dev/null
+++ b/infinigen/assets/wall_decorations/range_hood.py
@@ -0,0 +1,191 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo
+
+import bpy
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+import infinigen.core.util.blender as butil
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.assets.table_decorations.utils import nodegroup_lofting_poly
+from infinigen.assets.tables.table_utils import nodegroup_n_gon_profile
+from infinigen.assets.material_assignments import AssetList
+
+
+class RangeHoodFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, dimensions=None):
+ super(RangeHoodFactory, self).__init__(factory_seed, coarse=coarse)
+
+ self.dimensions = dimensions
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters(dimensions)
+ self.surface, self.scratch, self.edge_wear = self.get_material_params()
+
+
+ def get_material_params(self):
+ material_assignments = AssetList['RangeHoodFactory']()
+ surface = material_assignments['surface'].assign_material()
+
+ scratch_prob, edge_wear_prob = material_assignments['wear_tear_prob']
+ scratch, edge_wear = material_assignments['wear_tear']
+
+ is_scratch = np.random.uniform() < scratch_prob
+ is_edge_wear = np.random.uniform() < edge_wear_prob
+ if not is_scratch:
+ scratch = None
+
+ if not is_edge_wear:
+ edge_wear = None
+
+ return surface, scratch, edge_wear
+
+ @staticmethod
+ def sample_parameters(dimensions):
+ # all in meters
+ if dimensions is None:
+ x = 0.55
+ y = 0.75
+ z = 1.0
+ dimensions = (x, y, z)
+
+ x, y, z = dimensions
+
+ height_1 = uniform(0.05, 0.07)
+ height_2 = uniform(0.1, 0.3)
+ scale_2 = uniform(0.25, 0.4)
+
+ parameters = {
+ 'Height_total': z,
+ 'Width': y,
+ 'Depth': x,
+ 'Height_1': height_1,
+ 'Scale_2': scale_2,
+ 'Height_2': height_2
+ }
+
+ return parameters
+
+ def create_asset(self, **params):
+
+ bpy.ops.mesh.primitive_plane_add(
+ size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
+ obj = bpy.context.active_object
+
+ surface.add_geomod(obj, geometry_generate_hood, apply=True, input_kwargs=self.params)
+ butil.modify_mesh(obj, 'SOLIDIFY', apply=True, thickness=.002)
+ butil.modify_mesh(obj, 'SUBSURF', apply=True, levels=1, render_levels=1)
+
+ return obj
+
+ def finalize_assets(self, assets):
+ self.surface.apply(assets)
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+
+
+def geometry_generate_hood(nw: NodeWrangler, **kwargs):
+ # Code generated using version 2.6.4 of the node_transpiler
+
+ generatetabletop = nw.new_node(geometry_range_hood().name,
+ input_kwargs={'Resolution': 64,
+ 'Height_total': kwargs['Height_total'],
+ 'Width': kwargs['Width'],
+ 'Depth': kwargs['Depth'],
+ 'Height_1': kwargs['Height_1'],
+ 'Scale_2': kwargs['Scale_2'],
+ 'Height_2': kwargs['Height_2'],
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': generatetabletop}, attrs={'is_active_output': True})
+
+@node_utils.to_nodegroup('geometry_range_hood', singleton=False, type='GeometryNodeTree')
+def geometry_range_hood(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput,
+ expose_input=[('NodeSocketInt', 'Resolution', 128),
+ ('NodeSocketFloat', 'Height_total', 0.0000),
+ ('NodeSocketFloat', 'Width', 0.0000),
+ ('NodeSocketFloat', 'Depth', 0.0000),
+ ('NodeSocketFloat', 'Profile Fillet Ratio', 0.0100),
+ ('NodeSocketFloat', 'Height_1', 0.0000),
+ ('NodeSocketFloat', 'Scale_2', 0.0000),
+ ('NodeSocketFloat', 'Height_2', 0.3000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 1.4140}, attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["Width"]},
+ attrs={'operation': 'DIVIDE'})
+
+ ngonprofile = nw.new_node(nodegroup_n_gon_profile().name,
+ input_kwargs={'Profile Width': multiply, 'Profile Aspect Ratio': divide, 'Profile Fillet Ratio': group_input.outputs["Profile Fillet Ratio"]})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': ngonprofile, 'Count': group_input.outputs["Resolution"]})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_1})
+
+ transform_geometry = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': resample_curve, 'Translation': combine_xyz})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Height_1"]})
+
+ transform_geometry_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_geometry, 'Translation': combine_xyz_1})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': group_input.outputs["Height_2"]})
+
+ transform_geometry_2 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_geometry, 'Translation': combine_xyz_2, 'Scale': group_input.outputs["Scale_2"]})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Height_total"], 1: group_input.outputs["Height_2"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': subtract})
+
+ transform_geometry_3 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_geometry_2, 'Translation': combine_xyz_3})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_geometry_3, transform_geometry_2, transform_geometry_1, transform_geometry]})
+
+ lofting_poly = nw.new_node(nodegroup_lofting_poly().name,
+ input_kwargs={'Profile Curves': join_geometry, 'U Resolution': group_input.outputs["Resolution"], 'V Resolution': group_input.outputs["Resolution"]})
+
+ delete_geometry = nw.new_node(Nodes.DeleteGeometry,
+ input_kwargs={'Geometry': lofting_poly.outputs["Geometry"], 'Selection': lofting_poly.outputs["Top"]})
+
+ grid = nw.new_node(Nodes.MeshGrid,
+ input_kwargs={'Size X': group_input.outputs["Width"], 'Size Y': group_input.outputs["Depth"], 'Vertices X': group_input.outputs["Resolution"], 'Vertices Y': group_input.outputs["Resolution"]})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"]}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_2})
+
+ transform_geometry_4 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': grid.outputs["Mesh"], 'Translation': combine_xyz_4, 'Rotation': (-0.0698, 0.0000, 0.0000), 'Scale': (0.9800, 0.9800, 1.0000)})
+
+ transform_geometry_5 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': transform_geometry_4, 'Rotation': (0.1047, 0.0000, 0.0000), 'Scale': (0.9500, 0.9700, 1.0000)})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [delete_geometry, transform_geometry_5]})
+
+ transform_geometry_6 = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': join_geometry_1, 'Rotation': (0.0, 0.0000, -np.pi/2)})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry_6}, attrs={'is_active_output': True})
+
diff --git a/infinigen/assets/wall_decorations/skirting_board.py b/infinigen/assets/wall_decorations/skirting_board.py
new file mode 100644
index 000000000..ce85f9f30
--- /dev/null
+++ b/infinigen/assets/wall_decorations/skirting_board.py
@@ -0,0 +1,286 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Yiming Zuo, Lingjie Mei, Alexander Raistrick
+
+import logging
+
+import bmesh
+import bpy
+import mathutils
+import numpy as np
+from numpy.random import uniform, normal, randint, choice, randint
+from tqdm import tqdm
+
+from infinigen.assets.creatures.util.geometry.curve import Curve
+from infinigen.assets.utils.decorate import (
+ read_co, read_edge_length, remove_edges, read_edge_direction, read_edges,
+ remove_duplicate_edges,
+)
+from infinigen.assets.utils.draw import bezier_curve
+from infinigen.assets.utils.object import new_plane, join_objects
+from infinigen.core.constraints.example_solver.room import constants
+from infinigen.core.constraints.example_solver.room.constants import WALL_HEIGHT, DOOR_WIDTH, WALL_THICKNESS
+from infinigen.core.constraints.example_solver.room.types import get_room_level
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.surface import write_attr_data
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+
+import infinigen.core.util.blender as butil
+
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.materials.plastics import plastic_rough
+
+import shapely
+from shapely.geometry import Polygon, MultiPolygon
+from shapely import affinity
+from shapely.ops import unary_union
+
+from infinigen.assets.utils.shapes import polygon2obj, obj2polygon
+from infinigen.core import tagging, tags as t
+from shapely.plotting import plot_polygon
+
+logger = logging.getLogger(__name__)
+
+
+@node_utils.to_nodegroup('nodegroup_make_skirting_board_001', singleton=False, type='GeometryNodeTree')
+def nodegroup_make_skirting_board(nw: NodeWrangler, control_points):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(
+ Nodes.GroupInput,
+ expose_input=[('NodeSocketCollection', 'Parent', None),
+ ('NodeSocketFloat', 'Thickness', 0.0300),
+ ('NodeSocketFloat', 'Height', 0.1500),
+ ('NodeSocketFloatDistance', 'Resolution', 0.0050),
+ ('NodeSocketBool', 'Is Ceiling', False)]
+ )
+
+ collection_info = nw.new_node(Nodes.CollectionInfo, input_kwargs={'Collection': group_input.outputs["Parent"]})
+
+ mesh = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': collection_info})
+
+
+ quadrilateral = nw.new_node(
+ 'GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': group_input.outputs["Thickness"], 'Height': group_input.outputs["Height"]}
+ )
+
+ multiply = nw.new_node(
+ Nodes.Math, input_kwargs={0: group_input.outputs["Thickness"]}, attrs={'operation': 'MULTIPLY'}
+ )
+
+ multiply_1 = nw.new_node(
+ Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -0.5000}, attrs={'operation': 'MULTIPLY'}
+ )
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': multiply_1})
+
+ transform_geometry = nw.new_node(
+ Nodes.Transform, input_kwargs={'Geometry': quadrilateral, 'Translation': combine_xyz}
+ )
+
+ resample_curve_1 = nw.new_node(
+ Nodes.ResampleCurve,
+ input_kwargs={'Curve': transform_geometry, 'Count': 220, 'Length': group_input.outputs["Resolution"]},
+ attrs={'mode': 'LENGTH'}
+ )
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ greater_than = nw.new_node(Nodes.Compare, input_kwargs={0: separate_xyz.outputs["X"]})
+
+ multiply_2 = nw.new_node(
+ Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'}
+ )
+
+ map_range = nw.new_node(Nodes.MapRange, input_kwargs={'Value': separate_xyz.outputs["Y"], 1: multiply_2, 2: 0.0000})
+
+ float_curve = nw.new_node(Nodes.FloatCurve, input_kwargs={'Value': map_range.outputs["Result"]})
+ node_utils.assign_curve(float_curve.mapping.curves[0], control_points)
+
+ multiply_3 = nw.new_node(
+ Nodes.Math,
+ input_kwargs={0: float_curve, 1: group_input.outputs["Thickness"]},
+ attrs={'operation': 'MULTIPLY'}
+ )
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_3, 'Y': separate_xyz.outputs["Y"]})
+
+ set_position = nw.new_node(
+ Nodes.SetPosition,
+ input_kwargs={'Geometry': resample_curve_1, 'Selection': greater_than, 'Position': combine_xyz_1}
+ )
+
+ switch = nw.new_node(
+ Nodes.Switch,
+ input_kwargs={
+ 0: group_input.outputs["Is Ceiling"], 8: (-1.0000, 1.0000, 1.0000), 9: (-1.0000, -1.0000, -1.0000)
+ },
+ attrs={'input_type': 'VECTOR'}
+ )
+
+ transform_geometry_1 = nw.new_node(
+ Nodes.Transform, input_kwargs={'Geometry': set_position, 'Scale': switch.outputs[3]}
+ )
+
+ curve_to_mesh_1 = nw.new_node(
+ Nodes.CurveToMesh, input_kwargs={'Curve': mesh, 'Profile Curve': transform_geometry_1, 'Fill Caps': True}
+ )
+
+ set_shade_smooth = nw.new_node(
+ Nodes.SetShadeSmooth, input_kwargs={'Geometry': curve_to_mesh_1, 'Shade Smooth': False}
+ )
+
+ group_output = nw.new_node(
+ Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth}, attrs={'is_active_output': True}
+ )
+
+
+def apply_skirtingboard(nw: NodeWrangler, contour, is_ceiling=False, seed=None, thickness=.02):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ # TODO: randomize style / size / materials
+ if seed is None:
+ seed = randint(0, 10000)
+ with FixedSeed(seed):
+ thickness = uniform(.02, .05)
+ height = uniform(0.08, 0.15)
+ color = color_category('white')
+ roughness = uniform(0.5, 1.0)
+ n_peaks = randint(1, 4)
+ start_y = uniform(0.0, 0.5)
+ mid_x = uniform(0.2, 0.8)
+ peak_xs = np.sort(uniform(0.0, mid_x, size=n_peaks))
+ peak_ys = np.sort(uniform(start_y, 1.0, size=n_peaks))
+ control_points = [(0.0000, start_y)]
+ control_points += [(x, y) for x, y in zip(peak_xs, peak_ys)]
+ control_points += [(mid_x, 1.0000),
+ (1.0000, 1.0000)]
+
+ makeskirtingboard = nw.new_node(
+ nodegroup_make_skirting_board(control_points=control_points).name,
+ input_kwargs={
+ 'Parent': contour,
+ 'Resolution': 0.0010,
+ 'Thickness': thickness,
+ 'Height': height,
+ 'Is Ceiling': is_ceiling
+ }
+ )
+
+ makeskirtingboard = nw.new_node(
+ Nodes.SetMaterial,
+ input_kwargs={
+ 'Geometry': makeskirtingboard, 'Material': surface.shaderfunc_to_material(
+ plastic_rough.shader_rough_plastic, base_color=color, roughness=roughness
+ )
+ }
+ )
+
+ group_output = nw.new_node(
+ Nodes.GroupOutput, input_kwargs={'Geometry': makeskirtingboard}, attrs={'is_active_output': True}
+ )
+
+
+def make_skirtingboard_contour(objs: list[bpy.types.Object], tag: t.Subpart):
+ # make the outline curve
+
+ assert len(objs) > 0
+
+ objs = [
+ tagging.extract_tagged_faces(o, {tag, t.Subpart.Visible}, nonempty=True)
+ for o in list(objs)
+ ]
+
+ all_polys = []
+ all_zs = []
+ for floor_pieces in objs:
+ all_polys.append(obj2polygon(floor_pieces))
+ all_zs.append(read_co(floor_pieces)[:, -1] + floor_pieces.location[-1])
+
+ floor_z = np.mean(np.concatenate(all_zs))
+
+ boundary = unary_union(all_polys).buffer(.05, join_style='mitre').buffer(-.05, join_style='mitre')
+
+ if isinstance(boundary, Polygon):
+ boundaries = [boundary]
+ else:
+ boundaries = boundary.geoms
+
+ contours = []
+
+ for b in boundaries:
+ lr = b.exterior
+ o = linear_ring2curve(lr)
+ contours.append(o)
+ o.location[-1] += floor_z
+ butil.apply_transform(o, True)
+ for lr in b.interiors:
+ o = linear_ring2curve(lr, True)
+ contours.append(o)
+ o.location[-1] += floor_z
+ butil.apply_transform(o, True)
+ butil.delete(objs)
+ return contours
+
+
+def make_skirting_board(objs, tag, joined=True):
+ if joined:
+ seqs = list([o for o in objs if get_room_level(o.name.split('.')[0]) == i] for i in [0])
+ else:
+ seqs = [[o] for o in objs]
+
+ for s in seqs:
+ logger.debug(f'make_skirting_board for {len(objs)=} {tag=}')
+
+ try:
+ contours = make_skirtingboard_contour(s, tag)
+ except shapely.errors.GEOSException as e:
+ logger.warning(f'make_skirting_board({objs=}, {tag=}) failed with {e}, skipping')
+ return
+
+ obj = new_plane()
+ obj.name = "skirtingboard_" + tag.value
+
+ col = butil.put_in_collection(contours, 'contour')
+ kwargs = {
+ 'contour': col,
+ 'seed': np.random.randint(1e7),
+ 'is_ceiling': tag == t.Subpart.Ceiling
+ }
+ surface.add_geomod(obj, apply_skirtingboard, apply=True, input_kwargs=kwargs)
+
+ portal_cutters = butil.get_collection('placeholders:portal_cutters').objects
+ for p in portal_cutters:
+ if p.name.startswith('entrance') and int(p.location[-1] / WALL_HEIGHT - 1 / 2) == 0:
+ p.location[-1] -= WALL_HEIGHT / 2
+ butil.modify_mesh(
+ obj, 'BOOLEAN', object=p, operation='DIFFERENCE', use_self=True,
+ use_hole_tolerant=True
+ )
+ p.location[-1] += WALL_HEIGHT / 2
+ butil.delete_collection(col)
+ col = butil.get_collection("skirting")
+ butil.put_in_collection(obj, col)
+
+
+def linear_ring2curve(ring, reversed=False):
+ coords = ring.coords
+ if shapely.is_ccw(ring) == reversed:
+ coords = coords[::-1]
+ coords = np.array(coords)
+ lengths = np.linalg.norm(coords[:-1] - coords[1:], axis=-1)
+ invalid = np.sort(np.nonzero((np.abs(lengths - WALL_THICKNESS) < .02) | (np.abs(lengths - DOOR_WIDTH) < .02))[0])
+ ranges = -1, *invalid, len(coords)
+ curves = []
+ for l, r in zip(ranges[:-1], ranges[1:]):
+ x, y = np.array(coords[l + 1:r + 1]).T
+ if len(x) > 1:
+ curves.append(bezier_curve((x, y, 0), list(np.arange(len(x))), 1, False))
+ return join_objects(curves)
diff --git a/infinigen/assets/wall_decorations/wall_art.py b/infinigen/assets/wall_decorations/wall_art.py
new file mode 100644
index 000000000..8e2cc7497
--- /dev/null
+++ b/infinigen/assets/wall_decorations/wall_art.py
@@ -0,0 +1,90 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+from numpy.random import uniform
+
+from infinigen.assets.materials.art import Art
+from infinigen.assets.utils.object import join_objects, new_plane, new_bbox
+from infinigen.assets.utils.uv import wrap_sides
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util import blender as butil
+from infinigen.assets.material_assignments import AssetList
+
+
+class WallArtFactory(AssetFactory):
+
+ def __init__(self, factory_seed, coarse=False):
+ super(WallArtFactory, self).__init__(factory_seed, coarse)
+ with FixedSeed(self.factory_seed):
+ self.width = log_uniform(.4, 2)
+ self.height = log_uniform(.4, 2)
+ self.thickness = uniform(.02, .05)
+ self.depth = uniform(.01, .02)
+ self.frame_bevel_segments = np.random.choice([0, 1, 4])
+ self.frame_bevel_width = uniform(self.depth / 4, self.depth / 2)
+ self.material_assignments = AssetList['WallArtFactory']()
+ self.assign_materials()
+
+ def assign_materials(self):
+ # self.surface = Art(self.factory_seed)
+ assignments = self.material_assignments
+ self.surface = assignments['surface'].assign_material()
+ if self.surface == Art:
+ self.surface = self.surface(self.factory_seed)
+ self.frame_surface = assignments['frame'].assign_material()
+ is_scratch = uniform() < assignments['wear_tear_prob'][0]
+ is_edge_wear = uniform() < assignments['wear_tear_prob'][1]
+ self.scratch = assignments['wear_tear'][0] if is_scratch else None
+ self.edge_wear = assignments['wear_tear'][1] if is_edge_wear else None
+
+
+ def create_placeholder(self, **params):
+ return new_bbox(
+ -0.01,
+ 0.15,
+ -self.width / 2 - self.thickness,
+ self.width / 2 + self.thickness,
+ -self.height / 2 - self.thickness,
+ self.height / 2 + self.thickness,
+ )
+
+ def create_asset(self, placeholder, **params) -> bpy.types.Object:
+ obj = new_plane()
+ obj.scale = self.width / 2, self.height / 2, 1
+ obj.rotation_euler = np.pi / 2, 0, np.pi / 2
+ butil.apply_transform(obj, True)
+
+ frame = deep_clone_obj(obj)
+ wrap_sides(obj, self.surface, 'x', 'y', 'z')
+ butil.select_none()
+ with butil.ViewportMode(frame, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.delete(type='ONLY_FACE')
+ butil.modify_mesh(frame, 'SOLIDIFY', thickness=self.thickness, offset=1)
+ with butil.ViewportMode(frame, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.bridge_edge_loops()
+ butil.modify_mesh(frame, 'SOLIDIFY', thickness=self.depth, offset=1)
+ if self.frame_bevel_segments > 0:
+ butil.modify_mesh(frame, 'BEVEL', width=self.frame_bevel_width, segments=self.frame_bevel_segments)
+ self.frame_surface.apply(frame)
+ obj = join_objects([obj, frame])
+ return obj
+
+ def finalize_assets(self, assets):
+ if self.scratch:
+ self.scratch.apply(assets)
+ if self.edge_wear:
+ self.edge_wear.apply(assets)
+
+class MirrorFactory(WallArtFactory):
+ def __init__(self, factory_seed, coarse=False):
+ super(MirrorFactory, self).__init__(factory_seed, coarse)
+ self.material_assignments = AssetList['MirrorFactory']()
+ self.assign_materials()
diff --git a/infinigen/assets/wall_decorations/wall_shelf.py b/infinigen/assets/wall_decorations/wall_shelf.py
new file mode 100644
index 000000000..4b05b0e3b
--- /dev/null
+++ b/infinigen/assets/wall_decorations/wall_shelf.py
@@ -0,0 +1,126 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import bpy
+import numpy as np
+import shapely
+from numpy.random import uniform
+import shapely.affinity
+
+from infinigen.assets.materials import metal, plastic
+from infinigen.assets.materials.woods import wood
+from infinigen.assets.utils.decorate import read_edge_direction, select_edges, read_edge_center
+from infinigen.assets.utils.object import new_bbox, new_bbox_2d, join_objects
+from infinigen.assets.utils.shapes import polygon2obj
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.surface import write_attr_data
+from infinigen.core import tagging as t
+from infinigen.core.tags import Subpart
+
+from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.random import random_general as rg, log_uniform
+
+
+class WallShelfFactory(AssetFactory):
+ support_sides_ = 'weighted_choice', (.5, 'none'), (1, 'bottom'), (1, 'top'), (1.5, 'both')
+ support_margins = 'weighted_choice', (2, 0), (1, ('uniform', .0, .2))
+ support_ratios = 'weighted_choice', (2, 1), (1, ('uniform', .5, .9))
+ support_alphas = 'weighted_choice', (1, 1), (
+ 1, ('weighted_choice', (1, ('log_uniform', .4, .7)), (2, ('log_uniform', 1.5, 3)), (1, 10)))
+ support_joins = 'mitre', 'round', 'bevel'
+ plate_bevels = 'weighted_choice', (1, 'none'), (1, 'front'), (1, 'side')
+
+ plate_surfaces = 'weighted_choice', (2, wood), (1, metal)
+ support_surfaces = 'weighted_choice', (2, metal), (1, wood), (2, plastic)
+
+ def __init__(self, factory_seed, coarse=False):
+ super(WallShelfFactory, self).__init__(factory_seed, coarse)
+ self.support_side = rg(self.support_sides_)
+ self.support_margin = rg(self.support_margins)
+ if self.support_margin == 0:
+ n_support = np.random.choice([2, 3, 4], p=[.7, .2, .1])
+ else:
+ n_support = np.random.choice([2, 3], p=[.8, .2])
+ self.support_locs = np.linspace(-.5 + self.support_margin, .5 - self.support_margin, n_support)
+ self.length = log_uniform(.3, .8)
+ self.width = log_uniform(.1, .2)
+ match self.support_side:
+ case 'none':
+ self.thickness = log_uniform(.03, .08)
+ case _:
+ self.thickness = log_uniform(.01, .05)
+ self.support_width = log_uniform(.01, .015)
+ self.support_thickness = self.support_width * log_uniform(.4, 1.)
+ self.support_length = self.width * uniform(.7, 1.1)
+ self.plate_bevel = rg(self.plate_bevels)
+ self.support_join = np.random.choice(self.support_joins)
+ self.plate_surface = rg(self.plate_surfaces)
+ self.support_surface = rg(self.support_surfaces)
+
+ def create_placeholder(self, **kwargs) -> bpy.types.Object:
+ box = new_bbox(0, self.width, -self.length / 2, self.length / 2, -self.support_length, self.support_length)
+ plane = new_bbox_2d(0, self.width, -self.length / 2, self.length / 2, self.thickness / 2)
+ write_attr_data(plane, f'{t.PREFIX}{Subpart.SupportSurface.value}', np.ones(1).astype(bool), 'INT', 'FACE')
+ return join_objects([box, plane])
+
+ def create_asset(self, **params) -> bpy.types.Object:
+ obj = self.make_plate()
+ self.plate_surface.apply(obj)
+ if self.support_side != 'none':
+ support = self.make_support()
+ supports = [support] + [deep_clone_obj(support) for _ in range(len(self.support_locs) - 1)]
+ for s, l in zip(supports, self.support_locs):
+ s.location[1] = self.length * l
+ self.support_surface.apply(supports)
+ obj = join_objects([obj] + supports)
+ return obj
+
+ def make_plate(self):
+ obj = new_bbox(0, self.width, -self.length / 2, self.length / 2, -self.thickness / 2, self.thickness / 2)
+ c = read_edge_center(obj)
+ d = read_edge_direction(obj)
+ front = (np.abs(d[:, 1]) > .5) & (c[:, 0] > .1)
+ side = np.abs(d[:, 0]) > .5
+ match self.plate_bevel:
+ case 'front':
+ selection = front
+ case 'side':
+ selection = front + side
+ case _:
+ selection = np.zeros_like(front)
+ with butil.ViewportMode(obj, 'EDIT'):
+ select_edges(obj, selection)
+ bpy.ops.mesh.bevel(offset=uniform(.3, .5) * self.thickness, segments=np.random.randint(4, 9))
+ return obj
+
+ def make_support_contour(self):
+ l = shapely.LineString(np.array([(1, 0), (0, 0), (0, 1)]) * self.support_length)
+ theta = np.linspace(0, np.pi / 2, 31)
+ alpha = rg(self.support_alphas)
+ r = 1 / ((np.cos(theta) + 1e-6) ** alpha + (np.sin(theta) + 1e-6) ** alpha) ** (1 / alpha)
+ xy = r[:, np.newaxis] * np.stack([np.cos(theta), np.sin(theta)], -1)
+ d = shapely.LineString(xy * self.support_length * rg(self.support_ratios))
+ return shapely.union(l, d)
+
+ def make_support(self):
+ lines = []
+ if self.support_side in ['top', 'both']:
+ lines.append(self.make_support_contour())
+ if self.support_side in ['bottom', 'both']:
+ lines.append(shapely.affinity.scale(self.make_support_contour(), 1, -1, 1, (0, 0, 0)))
+
+ contour = shapely.union_all(lines).buffer(self.support_thickness / 2, join_style=self.support_join)
+ obj = polygon2obj(contour)
+ obj.rotation_euler[0] = np.pi / 2
+ obj.location = self.support_thickness / 2, -self.support_width / 2, 0
+ butil.apply_transform(obj, True)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_region_move(
+ TRANSFORM_OT_translate={
+ 'value': (0, self.support_width, 0)
+ }
+ )
+ return obj
diff --git a/infinigen/assets/weather/cloud/generate.py b/infinigen/assets/weather/cloud/generate.py
index 27dfe2aea..f132f01b7 100644
--- a/infinigen/assets/weather/cloud/generate.py
+++ b/infinigen/assets/weather/cloud/generate.py
@@ -20,7 +20,7 @@
from infinigen.core.util.random import random_general as rg
from infinigen.core.nodes.node_wrangler import Nodes
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
@gin.configurable
@@ -78,13 +78,18 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object:
return butil.spawn_empty('placeholder', disp_type='CUBE', s=self.max_scale)
def create_asset(self, distance, **kwargs):
+
cloud_type = np.random.choice(self.cloud_types)
+
resolution_min, resolution_max = self.resolutions[cloud_type]
resolution = max(1 - distance / self.max_distance, 0)
resolution = resolution * (resolution_max - resolution_min) + resolution_min
resolution = int(resolution)
+
new_cloud = cloud_type("Cloud", self.ref_cloud)
new_cloud = new_cloud.make_cloud(marching_cubes=False, resolution=resolution, )
+ butil.apply_transform(new_cloud)
+
tag_object(new_cloud, 'cloud')
return new_cloud
diff --git a/infinigen/assets/weather/particles.py b/infinigen/assets/weather/particles.py
index 9e4f89477..a57393c3c 100644
--- a/infinigen/assets/weather/particles.py
+++ b/infinigen/assets/weather/particles.py
@@ -18,7 +18,7 @@
from infinigen.core.nodes import node_utils
from infinigen.core.util import blender as butil
from infinigen.core.util.random import random_general
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup
+from infinigen.core.tagging import tag_object, tag_nodegroup
from infinigen.assets.materials import dirt
from infinigen.infinigen_gpl.surfaces import snow
diff --git a/infinigen/assets/windows/__init__.py b/infinigen/assets/windows/__init__.py
new file mode 100644
index 000000000..4aed7852b
--- /dev/null
+++ b/infinigen/assets/windows/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Hongyu Wen
+
+from .window import WindowFactory
\ No newline at end of file
diff --git a/infinigen/assets/windows/window.py b/infinigen/assets/windows/window.py
new file mode 100644
index 000000000..d01d38be6
--- /dev/null
+++ b/infinigen/assets/windows/window.py
@@ -0,0 +1,1035 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Hongyu Wen: primary author
+# - Alexander Raistrick: update window glass
+
+import bpy
+import random
+import mathutils
+import numpy as np
+from numpy.random import uniform as U, normal as N, randint as RI, uniform
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen.core.util.color import color_category
+from infinigen.core import surface
+from infinigen.core.util import blender as butil
+
+from infinigen.core.util.math import FixedSeed, clip_gaussian
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.assets.materials import metal_shader_list, wood_shader_list
+
+from infinigen.assets.utils.autobevel import BevelSharp
+
+def shader_window_glass(nw: NodeWrangler):
+
+ """ Non-refractive glass shader, since windows consist of a one-sided mesh currently and would not properly
+ refract-then un-refract the light
+ """
+
+ roughness = clip_gaussian(0, 0.015, 0, 0.03, 0.03)
+ transmission = uniform(0.05, 0.12)
+
+ # non-refractive glass
+ transparent_bsdf = nw.new_node(Nodes.TransparentBSDF)
+ shader = nw.new_node(Nodes.GlossyBSDF, input_kwargs={'Roughness': roughness})
+ shader = nw.new_node(Nodes.MixShader, input_kwargs={'Fac': transmission, 1: transparent_bsdf, 2: shader})
+
+ # complete pass-through for non-camera rays, for render efficiency
+ light_path = nw.new_node(Nodes.LightPath)
+ shader = nw.new_node(Nodes.MixShader, input_kwargs={'Fac': light_path.outputs["Is Camera Ray"], 1: transparent_bsdf, 2: shader})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': shader}, attrs={'is_active_output': True})
+
+class WindowFactory(AssetFactory):
+ def __init__(self, factory_seed, coarse=False, curtain=None, shutter=None):
+ super(WindowFactory, self).__init__(factory_seed, coarse=coarse)
+
+ with FixedSeed(factory_seed):
+ self.params = self.sample_parameters()
+ self.beveler = BevelSharp()
+ self.curtain = curtain
+ self.shutter = shutter
+
+ @staticmethod
+ def sample_parameters():
+ frame_width = U(0.05, 0.1)
+ sub_frame_width = U(0.01, frame_width)
+ sub_frame_h_amount = RI(1, 2)
+ sub_frame_v_amount = RI(1, 2)
+ glass_thickness = U(0.01, 0.03)
+
+ shutter_panel_radius = U(0.001, 0.003)
+ shutter_width = U(0.03, 0.05)
+ shutter_thickness = U(0.003, 0.007)
+ shutter_rotation = U(0, 1)
+ shutter_inverval = shutter_width + U(0.001, 0.003)
+
+ curtain_frame_depth = U(0.05, 0.1)
+ curtain_depth = U(0.03, curtain_frame_depth)
+ curtain_frame_radius = U(0.01, 0.02)
+
+ shader_frame_material_choice = random.choice(wood_shader_list)
+ shader_curtain_frame_material_choice = random.choice(metal_shader_list)
+ shader_curtain_material_choice = shader_curtain_material
+
+ params = {
+ "FrameWidth": frame_width,
+ "SubFrameWidth": sub_frame_width,
+ "SubPanelHAmount": sub_frame_h_amount,
+ "SubPanelVAmount": sub_frame_v_amount,
+ "GlassThickness": glass_thickness,
+ "CurtainFrameDepth": curtain_frame_depth,
+ "CurtainDepth": curtain_depth,
+ "CurtainFrameRadius": curtain_frame_radius,
+ "ShutterPanelRadius": shutter_panel_radius,
+ "ShutterWidth": shutter_width,
+ "ShutterThickness": shutter_thickness,
+ "ShutterRotation": shutter_rotation,
+ "ShutterInterval": shutter_inverval,
+ "FrameMaterial": surface.shaderfunc_to_material(shader_frame_material_choice, vertical=True),
+ "CurtainFrameMaterial": surface.shaderfunc_to_material(shader_curtain_frame_material_choice),
+ "CurtainMaterial": surface.shaderfunc_to_material(shader_curtain_material_choice),
+ "Material": surface.shaderfunc_to_material(shader_window_glass)
+ }
+ return params
+
+ def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter=None):
+ if dimensions is None:
+ width = U(1, 4)
+ height = U(1, 4)
+ frame_thickness = U(0.05, 0.15)
+ else:
+ width, height, frame_thickness = dimensions
+
+ panel_h_amount = RI(1, 2)
+ v_ = width / height * panel_h_amount
+ panel_v_amount = int(uniform(v_ * 1.6, v_ * 2.5))
+
+ if open is None:
+ open = U(0, 1) < 0.5
+
+ if shutter is None:
+ shutter = U(0, 1) < 0.5
+
+ if curtain is None:
+ curtain = U(0, 1) < 0.5
+ if curtain:
+ open = False
+ sub_frame_thickness = U(0.01, frame_thickness)
+
+ open = False # keep windows closed on generation, let articulation module handle this later on
+ open_type = RI(0, 3)
+ open_offset = 0
+ oe_offset = 0
+ if open_type == 0:
+ if frame_thickness < sub_frame_thickness * 2:
+ open_type = RI(1, 2)
+ else:
+ oe_offset = U(sub_frame_thickness / 2, (frame_thickness - 2 * sub_frame_thickness) / 2)
+ if open:
+ open_offset = U(0, width / panel_h_amount)
+ else:
+ open_offset = 0
+ open_h_angle = U(0, 0.3) if open_type == 1 and open else 0
+ open_v_angle = -U(0, 0.3) if open_type == 2 and open else 0
+
+ curtain_interval_number = int(width / U(0.08, 0.2))
+ curtain_mid_l = -U(0, width / 2)
+ curtain_mid_r = U(0, width / 2)
+ return {
+ **self.params,
+ "Width": width,
+ "Height": height,
+ "FrameThickness": frame_thickness,
+ "PanelHAmount": panel_h_amount,
+ "PanelVAmount": panel_v_amount,
+ "SubFrameThickness": sub_frame_thickness,
+ "OpenHAngle": open_h_angle,
+ "OpenVAngle": open_v_angle,
+ "OpenOffset": open_offset,
+ "OEOffset": oe_offset,
+ "Curtain": curtain,
+ "CurtainIntervalNumber": curtain_interval_number,
+ "CurtainMidL": curtain_mid_l,
+ "CurtainMidR": curtain_mid_r,
+ "Shutter": shutter,
+ }
+
+ def create_asset(self, dimensions=None, open=None, realized=True, **params):
+ obj = butil.spawn_cube()
+
+ butil.modify_mesh(
+ obj,
+ 'NODES',
+ node_group=nodegroup_window_geometry(),
+ ng_inputs=self.sample_asset_params(dimensions, open, self.curtain,self.shutter),
+ apply=realized
+ )
+
+ obj.rotation_euler[0] = np.pi / 2
+ butil.apply_transform(obj, True)
+ obj_ =deep_clone_obj(obj)
+ self.beveler(obj)
+ if max(obj.dimensions) > 8:
+ butil.delete(obj)
+ obj = obj_
+ else:
+ butil.delete(obj_)
+
+ bpy.ops.object.light_add(
+ type='AREA',
+ radius=1,
+ align='WORLD',
+ location=(0,0,0),
+ scale=(1,1,1)
+ )
+ portal = bpy.context.active_object
+
+ w, _, h = obj.dimensions
+ portal.scale = (w, h, 1)
+ portal.data.cycles.is_portal = True
+ portal.rotation_euler = (-np.pi/2, 0, 0)
+ butil.parent_to(portal, obj, no_inverse=True)
+ portal.hide_viewport = True
+
+ return obj
+
+
+@node_utils.to_nodegroup('nodegroup_window_geometry', singleton=True, type='GeometryNodeTree')
+def nodegroup_window_geometry(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input_1 = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Height', 2.0000), ('NodeSocketFloatDistance', 'FrameWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'FrameThickness', 0.1000), ('NodeSocketInt', 'PanelHAmount', 0),
+ ('NodeSocketInt', 'PanelVAmount', 0), ('NodeSocketFloatDistance', 'SubFrameWidth', 0.0500),
+ ('NodeSocketFloatDistance', 'SubFrameThickness', 0.0500), ('NodeSocketInt', 'SubPanelHAmount', 3),
+ ('NodeSocketInt', 'SubPanelVAmount', 2), ('NodeSocketFloat', 'GlassThickness', 0.0100),
+ ('NodeSocketFloat', 'OpenHAngle', 0.5000), ('NodeSocketFloat', 'OpenVAngle', 0.5000),
+ ('NodeSocketFloat', 'OpenOffset', 0.5000), ('NodeSocketFloat', 'OEOffset', 0.0500),
+ ('NodeSocketBool', 'Curtain', False), ('NodeSocketFloat', 'CurtainFrameDepth', 0.5000),
+ ('NodeSocketFloat', 'CurtainDepth', 0.0300), ('NodeSocketFloat', 'CurtainIntervalNumber', 20.0000),
+ ('NodeSocketFloatDistance', 'CurtainFrameRadius', 0.0100), ('NodeSocketFloat', 'CurtainMidL', -0.5000),
+ ('NodeSocketFloat', 'CurtainMidR', 0.5000), ('NodeSocketBool', 'Shutter', True),
+ ('NodeSocketFloatDistance', 'ShutterPanelRadius', 0.0050),
+ ('NodeSocketFloatDistance', 'ShutterWidth', 0.0500),
+ ('NodeSocketFloatDistance', 'ShutterThickness', 0.0050), ('NodeSocketFloat', 'ShutterRotation', 0.0000),
+ ('NodeSocketFloat', 'ShutterInterval', 0.0500), ('NodeSocketMaterial', 'FrameMaterial', None),
+ ('NodeSocketMaterial', 'CurtainFrameMaterial', None), ('NodeSocketMaterial', 'CurtainMaterial', None),
+ ('NodeSocketMaterial', 'Material', None)])
+
+ windowpanel = nw.new_node(nodegroup_window_panel().name, input_kwargs={
+ 'Width': group_input_1.outputs["Width"],
+ 'Height': group_input_1.outputs["Height"],
+ 'FrameWidth': group_input_1.outputs["FrameWidth"],
+ 'FrameThickness': group_input_1.outputs["FrameThickness"],
+ 'PanelWidth': group_input_1.outputs["FrameWidth"],
+ 'PanelThickness': group_input_1.outputs["FrameThickness"],
+ 'PanelHAmount': group_input_1.outputs["PanelHAmount"],
+ 'PanelVAmount': group_input_1.outputs["PanelVAmount"],
+ 'FrameMaterial': group_input_1.outputs["FrameMaterial"],
+ 'Material': group_input_1.outputs["Material"]
+ })
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input_1.outputs["FrameWidth"],
+ 1: group_input_1.outputs["PanelVAmount"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Width"], 1: multiply},
+ attrs={'operation': 'SUBTRACT'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: group_input_1.outputs["PanelVAmount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: divide, 1: group_input_1.outputs["SubFrameWidth"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input_1.outputs["FrameWidth"],
+ 1: group_input_1.outputs["PanelHAmount"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Height"], 1: multiply_1},
+ attrs={'operation': 'SUBTRACT'})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_2, 1: group_input_1.outputs["PanelHAmount"]},
+ attrs={'operation': 'DIVIDE'})
+
+ subtract_3 = nw.new_node(Nodes.Math, input_kwargs={0: divide_1, 1: group_input_1.outputs["SubFrameWidth"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ windowpanel_1 = nw.new_node(nodegroup_window_panel().name, input_kwargs={
+ 'Width': subtract_1,
+ 'Height': subtract_3,
+ 'FrameWidth': group_input_1.outputs["SubFrameWidth"],
+ 'FrameThickness': group_input_1.outputs["SubFrameThickness"],
+ 'PanelWidth': group_input_1.outputs["SubFrameWidth"],
+ 'PanelThickness': group_input_1.outputs["SubFrameThickness"],
+ 'PanelHAmount': group_input_1.outputs["SubPanelHAmount"],
+ 'PanelVAmount': group_input_1.outputs["SubPanelVAmount"],
+ 'WithGlass': True,
+ 'GlassThickness': group_input_1.outputs["GlassThickness"],
+ 'FrameMaterial': group_input_1.outputs["FrameMaterial"],
+ 'Material': group_input_1.outputs["Material"]
+ })
+
+ windowshutter = nw.new_node(nodegroup_window_shutter().name, input_kwargs={
+ 'Width': subtract_1,
+ 'Height': subtract_3,
+ 'FrameWidth': group_input_1.outputs["FrameWidth"],
+ 'FrameThickness': group_input_1.outputs["FrameThickness"],
+ 'PanelWidth': group_input_1.outputs["ShutterPanelRadius"],
+ 'PanelThickness': group_input_1.outputs["ShutterPanelRadius"],
+ 'ShutterWidth': group_input_1.outputs["ShutterWidth"],
+ 'ShutterThickness': group_input_1.outputs["ShutterThickness"],
+ 'ShutterInterval': group_input_1.outputs["ShutterInterval"],
+ 'ShutterRotation': group_input_1.outputs["ShutterRotation"],
+ 'FrameMaterial': group_input_1.outputs["FrameMaterial"]
+ })
+
+ switch = nw.new_node(Nodes.Switch,
+ input_kwargs={1: group_input_1.outputs["Shutter"], 14: windowpanel_1, 15: windowshutter
+ })
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Width"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide_2 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input_1.outputs["Width"],
+ 1: group_input_1.outputs["PanelVAmount"]
+ }, attrs={'operation': 'DIVIDE'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: divide_2}, attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: multiply_3})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Height"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide_3 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input_1.outputs["Height"],
+ 1: group_input_1.outputs["PanelHAmount"]
+ }, attrs={'operation': 'DIVIDE'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: divide_3}, attrs={'operation': 'MULTIPLY'})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: multiply_5})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Y': add_1})
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': switch.outputs[6], 'Translation': combine_xyz})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': transform})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input_1.outputs["PanelHAmount"],
+ 1: group_input_1.outputs["PanelVAmount"]
+ }, attrs={'operation': 'MULTIPLY'})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': multiply_6},
+ attrs={'domain': 'INSTANCE'})
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input_1.outputs["PanelHAmount"]})
+
+ divide_4 = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: reroute},
+ attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide_4}, attrs={'operation': 'FLOOR'})
+
+ add_2 = nw.new_node(Nodes.Math, input_kwargs={0: divide, 1: group_input_1.outputs["FrameWidth"]})
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: floor, 1: add_2}, attrs={'operation': 'MULTIPLY'})
+
+ modulo = nw.new_node(Nodes.Math,
+ input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: reroute},
+ attrs={'operation': 'MODULO'})
+
+ add_3 = nw.new_node(Nodes.Math, input_kwargs={0: divide_1, 1: group_input_1.outputs["FrameWidth"]})
+
+ multiply_8 = nw.new_node(Nodes.Math, input_kwargs={0: modulo, 1: add_3}, attrs={'operation': 'MULTIPLY'})
+
+ power = nw.new_node(Nodes.Math, input_kwargs={0: -1.0000, 1: floor}, attrs={'operation': 'POWER'})
+
+ multiply_9 = nw.new_node(Nodes.Math, input_kwargs={0: power, 1: group_input_1.outputs["OEOffset"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_7, 'Y': multiply_8, 'Z': multiply_9})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements.outputs["Geometry"],
+ 'Offset': combine_xyz_1
+ })
+
+ power_1 = nw.new_node(Nodes.Math, input_kwargs={0: -1.0000, 1: floor}, attrs={'operation': 'POWER'})
+
+ multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["OpenVAngle"], 1: power_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_10})
+
+ modulo_1 = nw.new_node(Nodes.Math, input_kwargs={0: floor, 1: 2.0000}, attrs={'operation': 'MODULO'})
+
+ multiply_11 = nw.new_node(Nodes.Math, input_kwargs={0: divide, 1: modulo_1},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_4 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: multiply_11})
+
+ modulo_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_8, 1: 2.0000}, attrs={'operation': 'MODULO'})
+
+ multiply_12 = nw.new_node(Nodes.Math, input_kwargs={0: divide_1, 1: modulo_2},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_5 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: multiply_12})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add_4, 'Y': add_5})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances, input_kwargs={
+ 'Instances': set_position,
+ 'Rotation': combine_xyz_3,
+ 'Pivot Point': combine_xyz_2
+ })
+
+ multiply_13 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["OpenHAngle"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_13})
+
+ multiply_14 = nw.new_node(Nodes.Math, input_kwargs={0: add_3, 1: -0.5000}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_14})
+
+ rotate_instances_1 = nw.new_node(Nodes.RotateInstances, input_kwargs={
+ 'Instances': rotate_instances,
+ 'Rotation': combine_xyz_5,
+ 'Pivot Point': combine_xyz_6
+ })
+
+ power_2 = nw.new_node(Nodes.Math, input_kwargs={0: -1.0000, 1: floor}, attrs={'operation': 'POWER'})
+
+ multiply_15 = nw.new_node(Nodes.Math, input_kwargs={0: power_2, 1: group_input_1.outputs["OpenOffset"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_15})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': rotate_instances_1, 'Offset': combine_xyz_4})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [windowpanel, set_position_1]})
+
+ multiply_16 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_17 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_16, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_18 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["CurtainFrameDepth"], 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ curtain = nw.new_node(nodegroup_curtain().name, input_kwargs={
+ 'Width': group_input_1.outputs["Width"],
+ 'Depth': group_input_1.outputs["CurtainDepth"],
+ 'Height': group_input_1.outputs["Height"],
+ 'IntervalNumber': group_input_1.outputs["CurtainIntervalNumber"],
+ 'Radius': group_input_1.outputs["CurtainFrameRadius"],
+ 'L1': multiply_17,
+ 'R1': group_input_1.outputs["CurtainMidL"],
+ 'L2': group_input_1.outputs["CurtainMidR"],
+ 'R2': multiply_16,
+ 'FrameDepth': multiply_18,
+ 'CurtainFrameMaterial': group_input_1.outputs["CurtainFrameMaterial"],
+ 'CurtainMaterial': group_input_1.outputs["CurtainMaterial"]
+ })
+
+ multiply_19 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["FrameThickness"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ add_6 = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input_1.outputs["CurtainFrameDepth"], 1: multiply_19})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': add_6})
+
+ transform_geometry = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': curtain, 'Translation': combine_xyz_7})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [transform_geometry, join_geometry]})
+
+ switch_1 = nw.new_node(Nodes.Switch, input_kwargs={
+ 1: group_input_1.outputs["Curtain"],
+ 14: join_geometry,
+ 15: join_geometry_1
+ })
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': switch_1.outputs[6]})
+
+ bounding_box = nw.new_node(Nodes.BoundingBox, input_kwargs={'Geometry': realize_instances})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={
+ 'Geometry': realize_instances,
+ 'Bounding Box': bounding_box.outputs["Bounding Box"]
+ }, attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_line_seq', singleton=False, type='GeometryNodeTree')
+def nodegroup_line_seq(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'Width', -1.0000),
+ ('NodeSocketFloat', 'Height', 0.5000), ('NodeSocketFloat', 'Amount', 0.5000)])
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': multiply_1})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Y': multiply_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz, 'End': combine_xyz_1})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': curve_line})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements, input_kwargs={
+ 'Geometry': geometry_to_instance,
+ 'Amount': group_input.outputs["Amount"]
+ }, attrs={'domain': 'INSTANCE'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: 1.0000})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Amount"], 1: 1.0000})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: add_1},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: divide}, attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements.outputs["Geometry"],
+ 'Offset': combine_xyz_2
+ })
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Curve': set_position},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_curtain', singleton=False, type='GeometryNodeTree')
+def nodegroup_curtain(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'Width', 0.5000),
+ ('NodeSocketFloat', 'Depth', 0.1000), ('NodeSocketFloatDistance', 'Height', 0.1000),
+ ('NodeSocketFloat', 'IntervalNumber', 0.5000), ('NodeSocketFloatDistance', 'Radius', 1.0000),
+ ('NodeSocketFloat', 'L1', 0.5000), ('NodeSocketFloat', 'R1', 0.0000), ('NodeSocketFloat', 'L2', 0.0000),
+ ('NodeSocketFloat', 'R2', 0.5000), ('NodeSocketFloat', 'FrameDepth', 0.0000),
+ ('NodeSocketMaterial', 'CurtainFrameMaterial', None), ('NodeSocketMaterial', 'CurtainMaterial', None)])
+
+ reroute_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Radius"]})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: reroute_1, 1: 2.0000}, attrs={'operation': 'MULTIPLY'})
+
+ ico_sphere = nw.new_node(Nodes.MeshIcoSphere, input_kwargs={'Radius': multiply, 'Subdivisions': 4})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: -1.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_1})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz, 'End': combine_xyz_1})
+
+ sample_curve_1 = nw.new_node(Nodes.SampleCurve, input_kwargs={'Curves': curve_line, 'Factor': 1.0000})
+
+ set_position_2 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': ico_sphere.outputs["Mesh"],
+ 'Offset': sample_curve_1.outputs["Position"]
+ })
+
+ combine_xyz_9 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_1, 'Z': group_input.outputs["FrameDepth"]})
+
+ curve_line_4 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_1, 'End': combine_xyz_9})
+
+ combine_xyz_8 = nw.new_node(Nodes.CombineXYZ,
+ input_kwargs={'X': multiply_2, 'Z': group_input.outputs["FrameDepth"]})
+
+ curve_line_3 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz, 'End': combine_xyz_8})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_line, curve_line_4, curve_line_3]})
+
+ curve_circle = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': group_input.outputs["Radius"]})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': join_geometry_3,
+ 'Profile Curve': curve_circle.outputs["Curve"],
+ 'Fill Caps': True
+ })
+
+ ico_sphere_1 = nw.new_node(Nodes.MeshIcoSphere, input_kwargs={'Radius': multiply, 'Subdivisions': 4})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve, input_kwargs={'Curves': curve_line})
+
+ set_position_3 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': ico_sphere_1.outputs["Mesh"],
+ 'Offset': sample_curve.outputs["Position"]
+ })
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [set_position_2, curve_to_mesh_1, set_position_3]})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -0.4700},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_3})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': join_geometry_2, 'Offset': combine_xyz_3})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': set_position_1,
+ 'Material': group_input.outputs["CurtainFrameMaterial"]
+ })
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["L1"]})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["R1"]})
+
+ curve_line_1 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_4, 'End': combine_xyz_5})
+
+ resample_curve = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line_1, 'Count': 200})
+
+ combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["L2"]})
+
+ combine_xyz_7 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': group_input.outputs["R2"]})
+
+ curve_line_2 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_6, 'End': combine_xyz_7})
+
+ resample_curve_1 = nw.new_node(Nodes.ResampleCurve, input_kwargs={'Curve': curve_line_2, 'Count': 200})
+
+ join_geometry_1 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [resample_curve, resample_curve_1]})
+
+ spline_parameter_1 = nw.new_node(Nodes.SplineParameter)
+
+ capture_attribute = nw.new_node(Nodes.CaptureAttribute, input_kwargs={
+ 'Geometry': join_geometry_1,
+ 2: spline_parameter_1.outputs["Factor"]
+ })
+
+ spline_parameter = nw.new_node(Nodes.SplineParameter)
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["IntervalNumber"], 1: 6.2800},
+ attrs={'operation': 'MULTIPLY'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: multiply_4, 1: group_input.outputs["Width"]},
+ attrs={'operation': 'DIVIDE'})
+
+ multiply_5 = nw.new_node(Nodes.Math, input_kwargs={0: spline_parameter.outputs["Length"], 1: divide},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_5, 1: 1.6800})
+
+ sine = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': 'SINE'})
+
+ multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: sine, 1: group_input.outputs["Depth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply_6})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': capture_attribute.outputs["Geometry"],
+ 'Offset': combine_xyz_2
+ })
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Height"]})
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': reroute, 'Height': 0.0020})
+
+ position = nw.new_node(Nodes.InputPosition)
+
+ separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position})
+
+ divide_1 = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["X"], 1: reroute},
+ attrs={'operation': 'DIVIDE'})
+
+ capture_attribute_1 = nw.new_node(Nodes.CaptureAttribute,
+ input_kwargs={'Geometry': quadrilateral, 2: divide_1})
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': set_position,
+ 'Profile Curve': capture_attribute_1.outputs["Geometry"]
+ })
+
+ combine_xyz_12 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': capture_attribute_1.outputs[2],
+ 'Y': capture_attribute.outputs[2]
+ })
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': curve_to_mesh,
+ 'Name': 'UVMap',
+ 3: combine_xyz_12
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT2'})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': store_named_attribute,
+ 'Material': group_input.outputs["CurtainMaterial"]
+ })
+
+ multiply_7 = nw.new_node(Nodes.Math, input_kwargs={0: reroute_1, 1: 1.3000},
+ attrs={'operation': 'MULTIPLY'})
+
+ curve_circle_1 = nw.new_node(Nodes.CurveCircle, input_kwargs={'Radius': multiply_7})
+
+ curve_to_mesh_2 = nw.new_node(Nodes.CurveToMesh, input_kwargs={
+ 'Curve': curve_line,
+ 'Profile Curve': curve_circle_1.outputs["Curve"]
+ })
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_3, 1: group_input.outputs["Radius"]})
+
+ combine_xyz_10 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_1})
+
+ set_position_4 = nw.new_node(Nodes.SetPosition,
+ input_kwargs={'Geometry': curve_to_mesh_2, 'Offset': combine_xyz_10})
+
+ difference = nw.new_node(Nodes.MeshBoolean, input_kwargs={'Mesh 1': set_material, 'Mesh 2': set_position_4})
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [set_material_1, difference.outputs["Mesh"]]})
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth,
+ input_kwargs={'Geometry': join_geometry, 'Shade Smooth': False})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_window_shutter', singleton=False, type='GeometryNodeTree')
+def nodegroup_window_shutter(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Height', 2.0000), ('NodeSocketFloatDistance', 'FrameWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'FrameThickness', 0.1000),
+ ('NodeSocketFloatDistance', 'PanelWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'PanelThickness', 0.1000),
+ ('NodeSocketFloatDistance', 'ShutterWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'ShutterThickness', 0.1000), ('NodeSocketFloat', 'ShutterInterval', 0.5000),
+ ('NodeSocketFloat', 'ShutterRotation', 0.0000), ('NodeSocketMaterial', 'FrameMaterial', None)])
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', input_kwargs={
+ 'Width': group_input.outputs["Width"],
+ 'Height': group_input.outputs["Height"]
+ })
+
+ sqrt = nw.new_node(Nodes.Math, input_kwargs={0: 2.0000}, attrs={'operation': 'SQRT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["FrameWidth"], 1: sqrt},
+ attrs={'operation': 'MULTIPLY'})
+
+ quadrilateral_1 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', input_kwargs={
+ 'Width': multiply,
+ 'Height': group_input.outputs["FrameThickness"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': quadrilateral, 'Profile Curve': quadrilateral_1})
+
+ subtract = nw.new_node(Nodes.Math,
+ input_kwargs={0: group_input.outputs["Width"], 1: group_input.outputs["FrameWidth"]},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': subtract,
+ 'Y': group_input.outputs["ShutterWidth"],
+ 'Z': group_input.outputs["ShutterThickness"]
+ })
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance',
+ input_kwargs={'Geometry': cube.outputs["Mesh"]})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: group_input.outputs["Height"],
+ 1: group_input.outputs["FrameWidth"]
+ }, attrs={'operation': 'SUBTRACT'})
+
+ divide = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: group_input.outputs["ShutterInterval"]},
+ attrs={'operation': 'DIVIDE'})
+
+ floor = nw.new_node(Nodes.Math, input_kwargs={0: divide}, attrs={'operation': 'FLOOR'})
+
+ shutter_number = nw.new_node(Nodes.Math, input_kwargs={0: floor, 1: 1.0000}, label='ShutterNumber',
+ attrs={'operation': 'SUBTRACT'})
+
+ duplicate_elements = nw.new_node(Nodes.DuplicateElements,
+ input_kwargs={'Geometry': geometry_to_instance, 'Amount': shutter_number},
+ attrs={'domain': 'INSTANCE'})
+
+ shutter_true_interval = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: floor},
+ label='ShutterTrueInterval', attrs={'operation': 'DIVIDE'})
+
+ multiply_1 = nw.new_node(Nodes.Math, input_kwargs={
+ 0: duplicate_elements.outputs["Duplicate Index"],
+ 1: shutter_true_interval
+ }, attrs={'operation': 'MULTIPLY'})
+
+ multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: -0.5000},
+ attrs={'operation': 'MULTIPLY'})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: multiply_2, 1: shutter_true_interval})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: multiply_1, 1: add})
+
+ combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_1})
+
+ set_position = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': duplicate_elements.outputs["Geometry"],
+ 'Offset': combine_xyz_1
+ })
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["ShutterRotation"]})
+
+ combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute})
+
+ rotate_instances = nw.new_node(Nodes.RotateInstances,
+ input_kwargs={'Instances': set_position, 'Rotation': combine_xyz_5})
+
+ multiply_3 = nw.new_node(Nodes.Math, input_kwargs={0: shutter_true_interval, 1: 2.0000},
+ attrs={'operation': 'MULTIPLY'})
+
+ subtract_2 = nw.new_node(Nodes.Math, input_kwargs={0: subtract_1, 1: multiply_3},
+ attrs={'operation': 'SUBTRACT'})
+
+ combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["PanelWidth"],
+ 'Y': subtract_2,
+ 'Z': group_input.outputs["PanelThickness"]
+ })
+
+ cube_1 = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz_2})
+
+ multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ShutterWidth"]},
+ attrs={'operation': 'MULTIPLY'})
+
+ combine_xyz_3 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': multiply_4})
+
+ curve_line = nw.new_node(Nodes.CurveLine, input_kwargs={'End': combine_xyz_3})
+
+ geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance',
+ input_kwargs={'Geometry': curve_line})
+
+ combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': reroute})
+
+ rotate_instances_1 = nw.new_node(Nodes.RotateInstances, input_kwargs={
+ 'Instances': geometry_to_instance_1,
+ 'Rotation': combine_xyz_4
+ })
+
+ realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': rotate_instances_1})
+
+ sample_curve = nw.new_node(Nodes.SampleCurve, input_kwargs={'Curves': realize_instances, 'Factor': 1.0000})
+
+ set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={
+ 'Geometry': cube_1.outputs["Mesh"],
+ 'Offset': sample_curve.outputs["Position"]
+ })
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_mesh, rotate_instances, set_position_1]})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': join_geometry_2,
+ 'Material': group_input.outputs["FrameMaterial"]
+ })
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth,
+ input_kwargs={'Geometry': set_material, 'Shade Smooth': False})
+
+ realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': set_shade_smooth})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances_1},
+ attrs={'is_active_output': True})
+
+
+@node_utils.to_nodegroup('nodegroup_window_panel', singleton=False, type='GeometryNodeTree')
+def nodegroup_window_panel(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloatDistance', 'Width', 2.0000),
+ ('NodeSocketFloatDistance', 'Height', 2.0000), ('NodeSocketFloatDistance', 'FrameWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'FrameThickness', 0.1000),
+ ('NodeSocketFloatDistance', 'PanelWidth', 0.1000),
+ ('NodeSocketFloatDistance', 'PanelThickness', 0.1000), ('NodeSocketInt', 'PanelHAmount', 0),
+ ('NodeSocketInt', 'PanelVAmount', 0), ('NodeSocketBool', 'WithGlass', False),
+ ('NodeSocketFloat', 'GlassThickness', 0.0000), ('NodeSocketMaterial', 'FrameMaterial', None),
+ ('NodeSocketMaterial', 'Material', None)])
+
+ quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', input_kwargs={
+ 'Width': group_input.outputs["Width"],
+ 'Height': group_input.outputs["Height"]
+ })
+
+ sqrt = nw.new_node(Nodes.Math, input_kwargs={0: 2.0000}, attrs={'operation': 'SQRT'})
+
+ multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["FrameWidth"], 1: sqrt},
+ attrs={'operation': 'MULTIPLY'})
+
+ quadrilateral_1 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', input_kwargs={
+ 'Width': multiply,
+ 'Height': group_input.outputs["FrameThickness"]
+ })
+
+ curve_to_mesh = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': quadrilateral, 'Profile Curve': quadrilateral_1})
+
+ add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["PanelHAmount"], 1: -1.0000})
+
+ lineseq = nw.new_node(nodegroup_line_seq().name, input_kwargs={
+ 'Width': group_input.outputs["Width"],
+ 'Height': group_input.outputs["Height"],
+ 'Amount': add
+ })
+
+ reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["PanelWidth"]})
+
+ subtract = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["PanelThickness"], 1: 0.0010},
+ attrs={'operation': 'SUBTRACT'})
+
+ quadrilateral_2 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': reroute, 'Height': subtract})
+
+ curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': lineseq, 'Profile Curve': quadrilateral_2})
+
+ add_1 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["PanelVAmount"], 1: -1.0000})
+
+ lineseq_1 = nw.new_node(nodegroup_line_seq().name, input_kwargs={
+ 'Width': group_input.outputs["Height"],
+ 'Height': group_input.outputs["Width"],
+ 'Amount': add_1
+ })
+
+ transform = nw.new_node(Nodes.Transform,
+ input_kwargs={'Geometry': lineseq_1, 'Rotation': (0.0000, 0.0000, 1.5708)})
+
+ subtract_1 = nw.new_node(Nodes.Math, input_kwargs={0: subtract, 1: 0.0010}, attrs={'operation': 'SUBTRACT'})
+
+ quadrilateral_3 = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral',
+ input_kwargs={'Width': reroute, 'Height': subtract_1})
+
+ curve_to_mesh_2 = nw.new_node(Nodes.CurveToMesh,
+ input_kwargs={'Curve': transform, 'Profile Curve': quadrilateral_3})
+
+ join_geometry_3 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_mesh_1, curve_to_mesh_2]})
+
+ join_geometry_2 = nw.new_node(Nodes.JoinGeometry,
+ input_kwargs={'Geometry': [curve_to_mesh, join_geometry_3]})
+
+ set_material_1 = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': join_geometry_2,
+ 'Material': group_input.outputs["FrameMaterial"]
+ })
+
+ combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={
+ 'X': group_input.outputs["Width"],
+ 'Y': group_input.outputs["Height"],
+ 'Z': group_input.outputs["GlassThickness"]
+ })
+
+ cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': combine_xyz})
+
+ store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={
+ 'Geometry': cube.outputs["Mesh"],
+ 'Name': 'uv_map',
+ 3: cube.outputs["UV Map"]
+ }, attrs={'domain': 'CORNER', 'data_type': 'FLOAT_VECTOR'})
+
+ set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={
+ 'Geometry': store_named_attribute,
+ 'Material': group_input.outputs["Material"]
+ })
+
+ join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [set_material, set_material_1]})
+
+ switch = nw.new_node(Nodes.Switch, input_kwargs={
+ 1: group_input.outputs["WithGlass"],
+ 14: set_material_1,
+ 15: join_geometry
+ })
+
+ set_shade_smooth = nw.new_node(Nodes.SetShadeSmooth,
+ input_kwargs={'Geometry': switch.outputs[6], 'Shade Smooth': False})
+
+ group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth},
+ attrs={'is_active_output': True})
+
+
+def shader_curtain_material(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': color_category('textile'),
+ 'Transmission': np.random.uniform(0, 1),
+ 'Transmission Roughness': 1.0
+ })
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_curtain_frame_material(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': (0.1840, 0.0000, 0.8000, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_frame_material(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF,
+ input_kwargs={'Base Color': (0.8000, 0.5033, 0.0057, 1.0000)})
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
+
+
+def shader_glass_material(nw: NodeWrangler):
+ # Code generated using version 2.6.5 of the node_transpiler
+
+ principled_bsdf = nw.new_node(Nodes.PrincipledBSDF, input_kwargs={
+ 'Base Color': (0.0094, 0.0055, 0.8000, 1.0000),
+ 'Roughness': 0.0000
+ })
+
+ material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': principled_bsdf},
+ attrs={'is_active_output': True})
diff --git a/infinigen/core/constraints/checks.py b/infinigen/core/constraints/checks.py
new file mode 100644
index 000000000..863dfb50c
--- /dev/null
+++ b/infinigen/core/constraints/checks.py
@@ -0,0 +1,126 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+import itertools
+import logging
+
+from tqdm import tqdm
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r
+)
+from infinigen.core.constraints.example_solver import propose_discrete, propose_relations
+from infinigen_examples import indoor_constraint_examples as ex
+
+logger = logging.getLogger(__name__)
+
+def iter_domains(node: cl.Node) -> typing.Iterator[r.Domain]:
+
+ match node:
+ case cl.ObjectSetExpression():
+ yield node, r.constraint_domain(node)
+ case cl.Expression() | cl.Problem():
+ for k, c in node.children():
+ yield from iter_domains(c)
+ case _:
+ raise ValueError(f'iter_domains found unmatched {type(node)=} {node=}')
+
+def bound_coverage(b: r.Bound, stages: dict[str, r.Domain]) -> list[str]:
+ return [
+ k
+ for k, f in stages.items()
+ if propose_discrete.active_for_stage(b.domain, f)
+ ]
+
+
+def check_coverage_errors(b: r.Bound, coverage: list, stages: dict[str, r.Domain]):
+
+ if len(coverage) == 0:
+ raise ValueError(f'Greedy stages did not cover all object classes! User specified bound {b} had {coverage=}')
+
+ if len(coverage) != 1:
+ raise ValueError(
+ f'Object class {b} was covered in more than one greedy stage! Got {coverage=}. Greedy stages must be non-overlapping'
+ )
+
+ gen_options = propose_discrete.lookup_generator(b.domain.tags)
+ if len(gen_options) < 1:
+ raise ValueError(f'Object class {b=} had {gen_options=}')
+
+ for k in coverage:
+ logger.debug(f'Checking coverage {k=} {b.domain=} {stages[k]=}')
+
+ if not b.domain.intersects(stages[k]):
+ continue
+ prop = b.domain.intersection(stages[k])
+
+ if prop.is_recursive():
+ raise ValueError(f'Found recursive prop domain {prop.tags=} {len(prop.relations)=}')
+ assert not prop.is_recursive(), prop.tags
+
+ if not len(prop.relations):
+ continue
+ first, remaining, implied = propose_relations.minimize_redundant_relations(prop.relations)
+ if implied:
+ continue
+ if isinstance(first[0], cl.AnyRelation):
+ raise ValueError(f'{b=} in {stages[k]=} had underspecified {first=}')
+
+def check_problem_greedy_coverage(prob: cl.Problem, stages: dict[str, r.Domain]):
+
+ bounds = r.constraint_bounds(prob)
+
+ for b in tqdm(bounds, desc="Checking greedy stages coverage"):
+ coverage = bound_coverage(b, stages)
+ check_coverage_errors(b, coverage, stages)
+
+def check_unfinalized_constraints(prob: cl.Problem):
+ # TODO
+ return []
+
+def check_contradictory_domains(prob: cl.Problem):
+
+ for node, dom in iter_domains(prob):
+ contradictory = not dom.satisfies(dom)
+ if contradictory:
+ raise ValueError(f'Constraint node had self-contradicting domain. \n{node=} \n{dom=}')
+
+def validate_stages(stages: dict[str, r.Domain]):
+
+ for k, d in stages.items():
+ if d.is_recursive():
+ raise ValueError(f'{k=} had recursive domain')
+
+ for (k1, d1), (k2, d2) in itertools.product(stages.items(), stages.items()):
+ inter = d1.intersects(d2)
+ if inter != (k1 == k2):
+ raise ValueError(
+ f"User provided greedy stages with keys {k1=} {k2=} which had non-empty intersection! "
+ " please define greedy stages which are mutually exclusive."
+ )
+
+def check_all(
+ prob: cl.Problem,
+ greedy_stages: dict[str, r.Domain],
+ all_vars: list[str]
+):
+
+ for k, v in greedy_stages.items():
+
+ if not isinstance(v, r.Domain):
+ raise TypeError(f'Greedy stage {k=} had non-domain value {v=}')
+
+ extras = v.all_vartags() - set(all_vars)
+ if len(extras):
+ raise ValueError(f'{k=} had extra vars {extras=}. Greedy domains may only contain vars from {all_vars}')
+
+ validate_stages(greedy_stages)
+
+ check_problem_greedy_coverage(prob, greedy_stages)
+ check_unfinalized_constraints(prob)
+ check_contradictory_domains(prob)
diff --git a/infinigen/core/constraints/constraint_language/__init__.py b/infinigen/core/constraints/constraint_language/__init__.py
new file mode 100644
index 000000000..5b386e1cc
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/__init__.py
@@ -0,0 +1,67 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick, Karhan Kayan
+
+from infinigen.core.tags import Semantics, Negated
+from .types import Node
+
+from .expression import (
+ Expression,
+ ArithmethicExpression,
+ constant,
+ ScalarOperatorExpression,
+ BoolOperatorExpression,
+ ScalarExpression,
+ BoolExpression,
+ hinge,
+ max_expr,
+ min_expr,
+)
+
+from .set_reasoning import (
+ scene,
+ tagged,
+ excludes,
+ count,
+ in_range,
+ related_to,
+)
+from .geometry import (
+ ObjectSetExpression,
+ distance,
+ min_distance_internal,
+ focus_score,
+ angle_alignment_cost,
+ freespace_2d,
+ min_dist_2d,
+ rotational_asymmetry,
+ center_stable_surface_dist,
+ accessibility_cost,
+ reflectional_asymmetry,
+ volume,
+ coplanarity_cost
+)
+from .result import Problem
+from .relations import (
+ Relation,
+ NegatedRelation,
+ AnyRelation,
+ ConnectorType,
+ RoomNeighbour,
+ CutFrom,
+ GeometryRelation,
+ Touching,
+ SupportedBy,
+ StableAgainst
+)
+from .gather import (
+ sum,
+ mean,
+ all,
+ item,
+ ForAll,
+ SumOver,
+ MeanOver
+)
\ No newline at end of file
diff --git a/infinigen/core/constraints/constraint_language/expression.py b/infinigen/core/constraints/constraint_language/expression.py
new file mode 100644
index 000000000..9dc8d2c1d
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/expression.py
@@ -0,0 +1,182 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+import operator
+from dataclasses import dataclass
+import functools
+import math
+
+from .types import Node, nodedataclass
+
+OPERATOR_ASSOCIATIVE = [
+ operator.add,
+ operator.mul,
+ operator.and_,
+ max,
+ min
+]
+
+@nodedataclass()
+class Expression(Node):
+
+ @classmethod
+ def register_postfix_func(cls, expr_cls):
+ @functools.wraps(expr_cls)
+ def postfix_instantiator(self, *args, **kwargs):
+ return expr_cls(self, *args, **kwargs)
+ setattr(cls, expr_cls.__name__, postfix_instantiator)
+ return expr_cls
+
+@nodedataclass()
+class ArithmethicExpression(Expression):
+ pass
+
+@nodedataclass()
+class ScalarExpression(ArithmethicExpression):
+
+ def minimize(self, *, weight: float):
+ return self * constant(weight)
+
+ def maximize(self, *, weight: float):
+ return self * constant(-weight)
+
+ def multiply(self, other):
+ return ScalarOperatorExpression(operator.mul, [self, other])
+ __mul__ = multiply
+
+ def abs(self):
+ return ScalarOperatorExpression(operator.abs, [self])
+ __abs__ = abs
+
+ def add(self, other):
+ return ScalarOperatorExpression(operator.add, [self, other])
+ __add__ = add
+
+ def sub(self, other):
+ return ScalarOperatorExpression(operator.sub, [self, other])
+ __sub__ = sub
+
+ def div(self, other):
+ return ScalarOperatorExpression(operator.truediv, [self, other])
+ __truediv__ = div
+
+ def pow(self, other):
+ return ScalarOperatorExpression(operator.pow, [self, other])
+ __pow__ = pow
+
+ def equals(self, other):
+ return BoolOperatorExpression(operator.eq, [self, other])
+ __eq__ = equals
+
+ def __ge__(self, other):
+ return BoolOperatorExpression(operator.ge, [self, other])
+ def __gt__(self, other):
+ return BoolOperatorExpression(operator.gt, [self, other])
+ def __le__(self, other):
+ return BoolOperatorExpression(operator.le, [self, other])
+ def __lt__(self, other):
+ return BoolOperatorExpression(operator.lt, [self, other])
+ def __ne__(self, other):
+ return BoolOperatorExpression(operator.ne, [self, other])
+
+ def __neg__(self):
+ return self * constant(-1)
+
+ def clamp_min(self, other):
+ return max_expr(self, other)
+ def clamp_max(self, other):
+ return min_expr(self, other)
+
+def max_expr(*args):
+ return ScalarOperatorExpression(max, args)
+
+def min_expr(*args):
+ return ScalarOperatorExpression(min, args)
+
+@nodedataclass()
+class BoolExpression(ArithmethicExpression):
+
+ def __mul__(self, other):
+ return BoolOperatorExpression(operator.and_, [self, other])
+
+
+@nodedataclass()
+class constant(ScalarExpression):
+ value: int
+
+ def __post_init__(self):
+ assert isinstance(self.value, (bool | float | int))
+
+ def __call__(self):
+ return self.value
+
+def _preprocess_operands(operands):
+ def cast_to_node(x):
+ match x:
+ case Node():
+ return x
+ case x if isinstance(x, (bool | float | int)):
+ return constant(x)
+ case _:
+ raise ValueError(f'Unsupported operand type {type(x)=} {x=}')
+ return [cast_to_node(x) for x in operands]
+
+def _collapse_associative(self, operands):
+
+ if self.func not in OPERATOR_ASSOCIATIVE:
+ return operands
+
+ new_operands = []
+ for op in operands:
+ if isinstance(op, self.__class__) and op.func == self.func:
+ new_operands.extend(op.operands)
+ else:
+ new_operands.append(op)
+ return new_operands
+
+@nodedataclass()
+class BoolOperatorExpression(BoolExpression):
+
+ func: typing.Callable
+ operands: list[Expression]
+
+ def __post_init__(self):
+ self.operands = _preprocess_operands(self.operands)
+ self.operands = _collapse_associative(self, self.operands)
+
+ def children(self):
+ for i, v in enumerate(self.operands):
+ yield f'operands[{i}]', v
+
+ def __call__(self) -> typing.Any:
+ return self.func(*[x() for x in self.operands])
+
+
+@nodedataclass()
+class ScalarOperatorExpression(ScalarExpression):
+
+ func: typing.Callable
+ operands: list[Expression]
+
+ def __post_init__(self):
+ self.operands = _preprocess_operands(self.operands)
+ self.operands = _collapse_associative(self, self.operands)
+ assert self.func not in [operator.and_, operator.or_]
+
+ def children(self):
+ for i, v in enumerate(self.operands):
+ yield f'operands[{i}]', v
+
+ def __call__(self) -> typing.Any:
+ return self.func(*[x() for x in self.operands])
+
+@ScalarExpression.register_postfix_func
+@nodedataclass()
+class hinge(ScalarExpression):
+ val: ScalarExpression
+ low: float
+ high: float
\ No newline at end of file
diff --git a/infinigen/core/constraints/constraint_language/gather.py b/infinigen/core/constraints/constraint_language/gather.py
new file mode 100644
index 000000000..071516d19
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/gather.py
@@ -0,0 +1,67 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+from dataclasses import dataclass, field
+
+from infinigen.core import tags as t
+from .relations import Relation
+from .expression import BoolExpression, ScalarExpression, nodedataclass
+from .geometry import ObjectSetExpression
+
+@nodedataclass()
+class item(ObjectSetExpression):
+ name: str
+ member_of: ObjectSetExpression
+
+ def __repr__(self):
+ return f'item({self.name})'
+
+ def children(self):
+ # member_of is metadata, should not be treated as a child
+ return []
+
+@nodedataclass()
+class ForAll(BoolExpression):
+ objs: ObjectSetExpression
+ var: str
+ pred: BoolExpression
+
+@ObjectSetExpression.register_postfix_func
+def all(
+ objs: ObjectSetExpression,
+ pred: typing.Callable[[item], BoolExpression]
+) -> BoolExpression:
+ var = 'var_all_' + str(id(pred))
+ return ForAll(objs, var, pred(item(var, objs)))
+
+@nodedataclass()
+class SumOver(ScalarExpression):
+ objs: ObjectSetExpression
+ var: str
+ pred: ScalarExpression
+
+@ObjectSetExpression.register_postfix_func
+def sum(
+ objs: ObjectSetExpression,
+ pred: typing.Callable[[item], ScalarExpression]
+):
+ var = 'var_sum_' + str(id(pred))
+ return SumOver(objs, var, pred(item(var, objs)))
+
+@nodedataclass()
+class MeanOver(ScalarExpression):
+ objs: ObjectSetExpression
+ var: str
+ pred: ScalarExpression
+
+@ObjectSetExpression.register_postfix_func
+def mean(
+ objs: ObjectSetExpression,
+ pred: typing.Callable[[item], ScalarExpression]
+):
+ var = 'var_mean_' + str(id(pred))
+ return MeanOver(objs, var, pred(item(var, objs)))
\ No newline at end of file
diff --git a/infinigen/core/constraints/constraint_language/geometry.py b/infinigen/core/constraints/constraint_language/geometry.py
new file mode 100644
index 000000000..2e4fd8851
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/geometry.py
@@ -0,0 +1,92 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import typing
+from dataclasses import dataclass, field
+
+import numpy as np
+
+from infinigen.core import tags as t
+from .relations import Relation
+from .expression import Expression, BoolExpression, ScalarExpression, nodedataclass
+from .set_reasoning import ObjectSetExpression
+
+@nodedataclass()
+class center_stable_surface_dist(ScalarExpression):
+ objs: ObjectSetExpression
+
+@nodedataclass()
+class accessibility_cost(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+ normal: np.array = field(default=np.array([1, 0, 0]))
+ dist: float = 1.0
+
+ def __post_init__(self):
+ if isinstance(self.normal, (list, tuple)):
+ self.normal = np.array(self.normal)
+ assert isinstance(self.normal, np.ndarray)
+
+@ObjectSetExpression.register_postfix_func
+@nodedataclass()
+class distance(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+ others_tags: set = field(default_factory=set)
+
+ def __post_init__(self):
+ assert isinstance(self.objs, ObjectSetExpression)
+ assert isinstance(self.others, ObjectSetExpression)
+ assert isinstance(self.others_tags, set)
+
+@nodedataclass()
+class min_distance_internal(ScalarExpression):
+ objs: ObjectSetExpression
+
+@nodedataclass()
+class focus_score(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+
+@nodedataclass()
+class angle_alignment_cost(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+ others_tags: set = None
+
+ def __post_init__(self):
+ if self.others_tags is None:
+ self.others_tags = set()
+ assert isinstance(self.others_tags, set), type(self.others_tags)
+
+@nodedataclass()
+class freespace_2d(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+
+@nodedataclass()
+class min_dist_2d(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+
+@nodedataclass()
+class rotational_asymmetry(ScalarExpression):
+ objs: ObjectSetExpression
+
+@nodedataclass()
+class reflectional_asymmetry(ScalarExpression):
+ objs: ObjectSetExpression
+ others: ObjectSetExpression
+ use_long_plane: bool = True
+
+@ObjectSetExpression.register_postfix_func
+@nodedataclass()
+class volume(ScalarExpression):
+ objs: ObjectSetExpression
+ dims: int | tuple = 3
+
+@nodedataclass()
+class coplanarity_cost(ScalarExpression):
+ objs: ObjectSetExpression
diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py
new file mode 100644
index 000000000..74483d0b0
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/relations.py
@@ -0,0 +1,414 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field, fields
+from enum import Enum
+from typing import Optional , Union
+from copy import deepcopy
+import logging
+
+from infinigen.core import tags as t
+from infinigen.core.constraints import constraint_language as cl
+
+logger = logging.getLogger(__name__)
+
+@dataclass(frozen=True)
+class Relation(ABC):
+
+ @abstractmethod
+ def implies(self, other) -> bool:
+ """
+ self must imply all parts of other, both positive and negative.
+ """
+ pass
+
+ @abstractmethod
+ def satisfies(self, other: Relation) -> bool:
+ """
+ self must imply all positive parts of other, and not contradict any negative parts
+ """
+ pass
+
+ @abstractmethod
+ def intersects(self, other, strict=False) -> bool:
+ pass
+
+ @abstractmethod
+ def intersection(self, other: Relation) -> Relation:
+ pass
+
+ @abstractmethod
+ def difference(self, other: Relation) -> Relation:
+ pass
+
+ def __neg__(self) -> Relation:
+ return NegatedRelation(self)
+
+@dataclass(frozen=True)
+class AnyRelation(Relation):
+
+ def implies(self, other) -> bool:
+ return other.__class__ is AnyRelation
+
+ def satisfies(self, other: cl.Relation) -> bool:
+ return other.__class__ is AnyRelation
+
+ def intersects(self, _other: Relation, strict=False) -> bool:
+ return True
+
+ def intersection(self, other: Relation) -> Relation:
+ return deepcopy(other)
+
+ def difference(self, other: Relation):
+ return -other
+
+@dataclass(frozen=True)
+class NegatedRelation(Relation):
+
+ rel: Relation
+
+ def __repr__(self):
+ return f'-{self.rel}'
+
+ def __str__(self):
+ return f'{self.__class__.__name__}({self.rel})'
+
+ def __neg__(self) -> Relation:
+ return self.rel
+
+ def implies(self, other: Relation) -> bool:
+ match other:
+ case AnyRelation():
+ return False
+ case NegatedRelation(rel):
+ return self.rel.implies(rel)
+ case _:
+ return (
+ not self.rel.implies(other)
+ and not self.intersects(other, strict=True)
+ )
+
+ def satisfies(self, other: cl.Relation) -> bool:
+ match other:
+ case AnyRelation():
+ return False
+ case NegatedRelation(rel):
+ return self.rel.satisfies(rel)
+ case _:
+ return (
+ not self.rel.satisfies(other)
+ and not self.intersects(other, strict=True)
+ )
+
+ def intersects(self, other: Relation, strict=False) -> bool:
+
+ match other:
+ case NegatedRelation(rel):
+ if isinstance(self.rel, AnyRelation) or isinstance(rel, AnyRelation):
+ return False
+ # TODO hacky, allows false positives for GeometryRelation. very unlikely to come up however
+ return True
+ case _:
+ # implementationn depends on other's type, let them handle it
+ return other.intersects(self, strict=strict)
+
+ def intersection(self, other: Relation) -> Relation:
+ return self.rel.difference(other)
+
+ def difference(self, other: Relation) -> Relation:
+ return self.rel.intersection(other)
+
+class ConnectorType(Enum):
+ Door = "door"
+ Open = "open"
+ Wall = "wall"
+
+@dataclass(frozen=True)
+class RoomNeighbour(Relation):
+ connector_types: frozenset[ConnectorType] = field(default_factory=frozenset)
+
+ def __post_init__(self):
+ if self.connector_types is not None:
+ object.__setattr__(self, 'connector_types', frozenset(self.connector_types))
+
+
+ def implies(self, other: Relation) -> bool:
+
+ if isinstance(other, AnyRelation):
+ return True
+
+ return (
+ isinstance(other, RoomNeighbour)
+ and self.connector_types.issuperset(other.connector_types)
+ )
+
+ def satisfies(self, other: Relation) -> bool:
+ return self.implies(other)
+
+ def intersects(self, other: Relation, strict=False) -> bool:
+
+ if isinstance(other, AnyRelation):
+ return True
+
+ return (
+ isinstance(other, RoomNeighbour)
+ and not self.connector_types.isdisjoint(other.connector_types)
+ )
+
+ def intersection(self, other: Relation) -> Relation:
+
+ if isinstance(other, AnyRelation):
+ return deepcopy(self)
+
+ return self.__class__(
+ connector_types=self.connector_types.intersection(other.connector_types)
+ )
+
+ def difference(self, other: Relation) -> Relation:
+
+ if isinstance(other, AnyRelation):
+ return -AnyRelation()
+
+ return self.__class__(
+ connector_types=self.connector_types.difference(other.connector_types)
+ )
+
+
+def no_frozenset_repr(self: GeometryRelation):
+ is_neg = lambda x: isinstance(x, t.Negated)
+ setrepr = lambda s: f'{{{", ".join(repr(x) for x in sorted(list(s), key=is_neg))}}}'
+ return f'{self.__class__.__name__}({setrepr(self.child_tags)}, {setrepr(self.parent_tags)})'
+
+@dataclass(frozen=True)
+class GeometryRelation(Relation):
+ child_tags: frozenset[t.Subpart] = field(default_factory=frozenset)
+ parent_tags: frozenset[t.Subpart] = field(default_factory=frozenset)
+
+ __repr__ = no_frozenset_repr
+
+ def __post_init__(self):
+ # allow the user to init with sets that subsequently get frozen
+ # use object.__setattr__ to bypass dataclass's frozen since it is guaranteed safe here
+ object.__setattr__(self, 'child_tags', frozenset(self.child_tags))
+ object.__setattr__(self, 'parent_tags', frozenset(self.parent_tags))
+
+ def _extra_fields(self) -> list[str]:
+ """Return any fields added by subclasses. Useful for implementing implies/intersects
+ which must check these fields regardless of inheritance. TODO, Hacky.
+ """
+ return [
+ f.name
+ for f in fields(self)
+ if f.name not in ['child_tags', 'parent_tags']
+ ]
+
+ def _compatibility_checks(self, other: GeometryRelation, strict_on_fields=False) -> bool:
+
+ if not issubclass(other.__class__, self.__class__):
+ return False
+
+ if strict_on_fields:
+ for k in self._extra_fields():
+ if not getattr(self, k) == getattr(other, k):
+ #logger.warning(f'{self._compatibility_checks} ignoring mismatch {k=} for {other=}')
+ return False
+
+ return True
+
+ def implies(self, other: Relation) -> bool:
+
+ match other:
+ case AnyRelation():
+ return True
+ case NegatedRelation(AnyRelation()):
+ return False
+ case GeometryRelation(ochild, oparent):
+ if not self._compatibility_checks(other):
+ logger.debug(f'{self.implies} failed compatibility for %s', other)
+ return False
+ if not t.implies(self.child_tags, ochild):
+ logger.debug(f'{self.implies} failed child tags for %s', other)
+ return False
+ if not t.implies(self.parent_tags, oparent):
+ logger.debug(f'{self.implies} failed parent tags for %s', other)
+ return False
+ return True
+ case NegatedRelation(GeometryRelation(ochild, oparent)):
+ if not self._compatibility_checks(other.rel):
+ logger.debug(f'{self.implies} failed compatibility for %s', other)
+ return False
+ if (
+ t.implies(self.child_tags, {-t for t in ochild})
+ and t.implies(self.parent_tags, {-t for t in oparent})
+ ):
+ return True
+ return False
+ case _:
+ raise ValueError(f'{self.implies} encountered unhandled {other=}')
+
+ def satisfies(self, other: Relation) -> bool:
+ match other:
+ case AnyRelation():
+ return True
+ case NegatedRelation(AnyRelation()):
+ return False
+ case GeometryRelation(ochild, oparent):
+ if not self._compatibility_checks(other):
+ logger.debug(f'{self.satisfies} failed compatibility for %s', other)
+ return False
+ if not t.satisfies(self.child_tags, ochild):
+ logger.debug(f'{self.satisfies} failed child tags for %s', other)
+ return False
+ if not t.satisfies(self.parent_tags, oparent):
+ logger.debug(f'{self.satisfies} failed parent tags for %s', other)
+ return False
+ return True
+ case NegatedRelation(GeometryRelation(ochild, oparent)):
+ if not self._compatibility_checks(other.rel):
+ logger.debug(f'{self.implies} failed compatibility for %s', other)
+ return False
+ if (
+ t.satisfies(self.child_tags, {-t for t in ochild})
+ and t.satisfies(self.parent_tags, {-t for t in oparent})
+ ):
+ return True
+ return False
+ case _:
+ raise ValueError(f'{self.satisfies} encountered unhandled {other=}')
+
+ def intersects(self, other: Relation, strict=False) -> bool:
+
+ def tags_compatible(a, b):
+ if strict:
+ return t.implies(a, b) or t.implies(b, a)
+ else:
+ return not t.contradiction(a.union(b))
+
+ logger.debug(f'{self.intersects} other=%s', other)
+
+ match other:
+ case AnyRelation():
+ return True
+ case NegatedRelation(AnyRelation()):
+ return False
+ case GeometryRelation(ochild, oparent):
+ if not self._compatibility_checks(other):
+ logger.debug(f'{self.intersects} failed compatibility for other=%s', other)
+ return False
+ if not tags_compatible(self.child_tags, ochild):
+ logger.debug(f'{self.intersects} failed child tags for other=%s', other)
+ return False
+ if not tags_compatible(self.parent_tags, oparent):
+ logger.debug('{self.intersects} failed parent tags for other=%s', other)
+ return False
+ return True
+ case NegatedRelation(GeometryRelation()):
+ # is self compatible with NOT other.rel?
+ # true unless other.rel->self
+ return not other.rel.implies(self)
+ case _:
+ logger.warning(f'{self.intersects} encountered unhandled %s, returning False', other)
+ return False
+
+ def intersection(self: Relation, other: Relation) -> Relation:
+
+ """ TODO: There are potentially many intersections of relations with negations.
+ """
+
+ match other:
+ case AnyRelation():
+ return deepcopy(self)
+ case NegatedRelation(rel):
+ return self.difference(rel)
+ case GeometryRelation(ochild, oparent):
+ if not self._compatibility_checks(other):
+ logger.warning(f'{self.intersection} failed compatibility for {other=}')
+ return -AnyRelation()
+ return self.__class__(
+ child_tags=self.child_tags.union(ochild),
+ parent_tags=self.parent_tags.union(oparent),
+ **{k: getattr(self, k) for k in self._extra_fields()}
+ )
+ case _:
+ logger.warning(f'Encountered unhandled {other=} for {self.intersection}')
+ return -AnyRelation()
+
+ def difference(self: Relation, other: Relation) -> Relation:
+
+ match other:
+ case AnyRelation():
+ return -AnyRelation()
+ case NegatedRelation(rel):
+ return self.intersection(rel)
+ case GeometryRelation(ochild, oparent):
+ if not self.intersects(other):
+ return deepcopy(self)
+ if (
+ t.implies(self.child_tags, ochild)
+ and t.implies(self.parent_tags, oparent)
+ ):
+ return -AnyRelation()
+
+ return self.__class__(
+ child_tags=t.difference(self.child_tags, ochild),
+ parent_tags=t.difference(self.parent_tags, oparent),
+ **{k: getattr(self, k) for k in self._extra_fields()}
+ )
+ case _:
+ logger.warning(f'Encountered unhandled {other=} for {self.intersection}')
+ return -AnyRelation()
+
+@dataclass(frozen=True)
+class Touching(GeometryRelation):
+ __repr__ = no_frozenset_repr
+
+
+@dataclass(frozen=True)
+class SupportedBy(Touching):
+ __repr__ = no_frozenset_repr
+
+
+@dataclass(frozen=True)
+class StableAgainst(GeometryRelation):
+ margin: float = 0
+
+ # check_ if False, only check x/z stability, z is allowed to overhand.
+ # typical use is chair-against-table relation
+ check_z: bool = True
+
+ # rev_normal: if True, align the normals so they face the SAME direction, rather than two planes facing eachother.
+ # typical use is for sink embedded in countertop
+ rev_normal: bool = False
+
+ __repr__ = no_frozenset_repr
+
+
+@dataclass(frozen=True)
+class CutFrom(Relation):
+
+ def implies(self, other: Relation) -> bool:
+ return (
+ isinstance(other, AnyRelation)
+ or isinstance(other, CutFrom)
+ )
+
+ def satisfies(self, other: Relation) -> bool:
+ return self.implies(other)
+
+ def intersects(self, other: Relation, strict=False) -> bool:
+ return (
+ isinstance(other, AnyRelation)
+ or isinstance(other, CutFrom)
+ )
+
+ def intersection(self, other: Relation) -> Relation:
+ return deepcopy(self)
+
+ def difference(self, other: Relation) -> Relation:
+ return -AnyRelation()
diff --git a/infinigen/core/constraints/constraint_language/result.py b/infinigen/core/constraints/constraint_language/result.py
new file mode 100644
index 000000000..f58c59c4d
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/result.py
@@ -0,0 +1,32 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+from dataclasses import dataclass, field
+
+import numpy as np
+
+from .types import Node
+from .expression import BoolExpression, ScalarExpression, nodedataclass
+
+@nodedataclass()
+class Problem(Node):
+
+ constraints: dict[str, BoolExpression]
+ score_terms: dict[str, ScalarExpression]
+
+ def __post_init__(self):
+
+ if isinstance(self.constraints, list):
+ self.constraints = {i: c for i, c in enumerate(self.constraints)}
+ if isinstance(self.score_terms, list):
+ self.score_terms = {i: s for i, s in enumerate(self.score_terms)}
+
+ def children(self):
+ for i, v in enumerate(self.constraints.values()):
+ yield f'constraints[{i}]', v
+ for i, v in enumerate(self.score_terms.values()):
+ yield f'score_terms[{i}]', v
\ No newline at end of file
diff --git a/infinigen/core/constraints/constraint_language/set_reasoning.py b/infinigen/core/constraints/constraint_language/set_reasoning.py
new file mode 100644
index 000000000..b87df0e36
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/set_reasoning.py
@@ -0,0 +1,83 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+from dataclasses import dataclass, field
+
+from infinigen.core import tags as t
+from infinigen.core.constraints import usage_lookup
+from .relations import Relation, AnyRelation
+from .expression import Expression, BoolExpression, ScalarExpression, nodedataclass
+
+@nodedataclass()
+class ObjectSetExpression(Expression):
+
+ def __getitem__(self, key):
+ return tagged(self, key)
+
+@nodedataclass()
+class scene(ObjectSetExpression):
+ pass
+
+@ObjectSetExpression.register_postfix_func
+@nodedataclass()
+class tagged(ObjectSetExpression):
+ objs: ObjectSetExpression
+ tags: set[t.Tag] = field(default_factory=set)
+
+ def __post_init__(self):
+ self.tags = t.to_tag_set(self.tags, fac_context=usage_lookup._factory_lookup)
+
+
+@ObjectSetExpression.register_postfix_func
+def excludes(objs, tags):
+
+ # syntactic helper - assume people wont construct obvious contradictions
+ if isinstance(objs, tagged):
+ tags = tags.difference(objs.tags)
+
+ return tagged(objs, {t.Negated(x) for x in tags})
+
+@ObjectSetExpression.register_postfix_func
+@nodedataclass()
+class related_to(ObjectSetExpression):
+ child: ObjectSetExpression
+ parent: ObjectSetExpression
+ relation: Relation = field(default_factory=AnyRelation)
+
+ def __post_init__(self):
+ if not isinstance(self.child, ObjectSetExpression):
+ raise TypeError(f'related_to got {self.child=}, must be an ObjectSetExpression')
+ if not isinstance(self.parent, ObjectSetExpression):
+ raise TypeError(f'related_to got {self.parent=}, must be an ObjectSetExpression')
+ if not isinstance(self.relation, Relation):
+ raise TypeError(f'related_to got {self.relation=}, must be a Relation')
+
+@ObjectSetExpression.register_postfix_func
+@nodedataclass()
+class count(ScalarExpression):
+ objs: ObjectSetExpression
+
+ def __post_init__(self):
+ if not isinstance(self.objs, ObjectSetExpression):
+ raise TypeError(f'count got {self.objs=}, must be an ObjectSetExpression')
+
+
+@ScalarExpression.register_postfix_func
+@nodedataclass()
+class in_range(BoolExpression):
+ val: ScalarExpression
+ low: float
+ high: float
+
+ def __post_init__(self):
+ if not isinstance(self.val, ScalarExpression):
+ raise TypeError(f'in_range got {self.val=}, must be a ScalarExpression')
+ if not isinstance(self.low, (int, float)):
+ raise TypeError(f'in_range got {self.low=}, must be a number')
+ if not isinstance(self.high, (int, float)):
+ raise TypeError(f'in_range got {self.high=}, must be a number')
+
diff --git a/infinigen/core/constraints/constraint_language/types.py b/infinigen/core/constraints/constraint_language/types.py
new file mode 100644
index 000000000..d6e07686d
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/types.py
@@ -0,0 +1,45 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from enum import Enum
+from typing import Any
+from dataclasses import dataclass
+import functools
+
+nodedataclass_kwargs = dict(eq=False, order=False)
+
+def _nodeclass_bool_throw(self):
+ raise RuntimeError(
+ f'Attempted to convert {self.__class__} to bool, '
+ f"truth value of {self} is ambiguous. Constraint language must use * instead of `and`, etc since python bool ops are not overridable"
+ )
+
+def nodedataclass(frozen=False):
+ def decorator(cls):
+ ddec = dataclass(eq=False, order=False, frozen=frozen)
+ cls = ddec(cls)
+ cls.__bool__ = _nodeclass_bool_throw
+ return cls
+ return decorator
+
+@nodedataclass()
+class Node:
+
+ def children(self):
+ for k, v in self.__dict__.items():
+ if isinstance(v, Node):
+ yield k, v
+
+ def traverse(self, inorder=True):
+ if inorder:
+ yield self
+ for _, c in self.children():
+ yield from c.traverse(inorder=inorder)
+ if not inorder:
+ yield self
+
+ def size(self):
+ return len(list(self.traverse()))
diff --git a/infinigen/core/constraints/constraint_language/util.py b/infinigen/core/constraints/constraint_language/util.py
new file mode 100644
index 000000000..8d5d0bffc
--- /dev/null
+++ b/infinigen/core/constraints/constraint_language/util.py
@@ -0,0 +1,404 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+from typing import Union
+import random
+import math
+import functools
+import logging
+
+import bpy
+import mathutils
+from trimesh import Trimesh, Scene
+import trimesh
+from shapely import LineString, Point, Polygon, MultiPolygon
+import numpy as np
+from sklearn.decomposition import PCA
+import bpy
+import fcl
+
+from mathutils import Matrix, Vector
+import gin
+
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+logger = logging.getLogger(__name__)
+
+@gin.configurable
+def bvh_caching_config(enabled=True):
+ return enabled
+
+@functools.cache
+def group(scene, x):
+ if isinstance(x, (list, set)):
+ x = tuple(x)
+ return subset(scene, x)
+
+def meshes_from_names(scene, names):
+ if isinstance(names, str):
+ names = [names]
+ return [scene.geometry[g] for _, g in (scene.graph[n] for n in names)]
+
+def blender_objs_from_names(names):
+ if isinstance(names, str):
+ names = [names]
+ return [bpy.data.objects[n] for n in names]
+
+def name_from_mesh(scene, mesh):
+ mesh_name = None
+ for name, mesh in scene.geometry.items():
+ if mesh == mesh:
+ mesh_name = name
+ break
+ return mesh_name
+
+def project_to_xy_path2d(mesh: trimesh.Trimesh) -> trimesh.path.Path2D:
+ poly = trimesh.path.polygons.projected(mesh, (0,0,1), (0,0,0))
+ d = trimesh.path.exchange.misc.polygon_to_path(poly)
+ return trimesh.path.Path2D(entities = d['entities'], vertices = d['vertices'])
+
+def project_to_xy_poly(mesh: trimesh.Trimesh):
+ poly = trimesh.path.polygons.projected(mesh, (0,0,1), (0,0,0))
+ return poly
+
+
+def closest_edge_to_point_poly(polygon, point):
+ closest_distance = float('inf')
+ closest_edge = None
+
+ for i, coord in enumerate(polygon.exterior.coords[:-1]):
+ start, end = coord, polygon.exterior.coords[i + 1]
+ line = LineString([start, end])
+ distance = line.distance(point)
+
+ if distance < closest_distance:
+ closest_distance = distance
+ closest_edge = line
+
+ return closest_edge
+
+def closest_edge_to_point_edge_list(edge_list: list[LineString], point):
+ closest_distance = float('inf')
+ closest_edge = None
+
+ for line in edge_list:
+ distance = line.distance(point)
+
+ if distance < closest_distance:
+ closest_distance = distance
+ closest_edge = line
+
+ return closest_edge
+
+def compute_outward_normal(line, polygon):
+ dx = line.xy[0][1] - line.xy[0][0] # x1 - x0
+ dy = line.xy[1][1] - line.xy[1][0] # y1 - y0
+
+ # Candidate normal vectors (perpendicular to edge)
+ normal_vector_1 = np.array([dy, -dx])
+ normal_vector_2 = -normal_vector_1
+
+ # Normalize the vectors (optional but recommended for consistency)
+ normal_vector_1 = normal_vector_1 / np.linalg.norm(normal_vector_1)
+ normal_vector_2 = normal_vector_2 / np.linalg.norm(normal_vector_2)
+
+ # Midpoint of the line segment
+ mid_point = line.interpolate(0.5, normalized=True)
+
+ # Move a tiny bit in the direction of the normals to check which points outside
+ test_point_1 = mid_point.coords[0] + 0.01 * normal_vector_1
+ test_point_2 = mid_point.coords[0] + 0.01 * normal_vector_2
+
+ # Return the normal for which the test point lies outside the polygon
+ if polygon.contains(Point(test_point_1)):
+ return normal_vector_2
+ else:
+ return normal_vector_1
+
+
+def get_transformed_axis(scene, obj_name):
+ obj = bpy.data.objects[obj_name]
+ trimesh_mesh = meshes_from_names(scene, obj_name)[0]
+ axis = trimesh_mesh.axis
+ rot_mat = np.array(obj.matrix_world.to_3x3())
+ return rot_mat @ np.array(axis)
+
+
+def set_axis(scene, objs: Union[str, list[str]], canonical_axis):
+ if isinstance(objs, str):
+ objs = [objs]
+ obj_meshes = meshes_from_names(scene, objs)
+ for obj_name, obj in zip(objs, obj_meshes):
+ obj.axis = canonical_axis
+ obj.axis = get_transformed_axis(scene, obj_name)
+
+
+
+
+def get_plane_from_3dmatrix(matrix):
+ """Extract the plane_normal and plane_origin from a transformation matrix."""
+ # The normal of the plane can be extracted from the 3x3 rotation part of the matrix
+ plane_normal = matrix[:3, 2]
+ plane_origin = matrix[:3, 3]
+ return plane_normal, plane_origin
+
+
+def project_points_onto_plane(points, plane_origin, plane_normal):
+ """Project 3D points onto a plane."""
+ d = np.dot(points - plane_origin, plane_normal)[:, None]
+ return points - d * plane_normal
+
+def to_2d_coordinates(points, plane_normal):
+ """Convert 3D points to 2D using the plane defined by its normal."""
+ # Compute two perpendicular vectors on the plane
+ u = np.cross(plane_normal, [1, 0, 0])
+ if np.linalg.norm(u) < 1e-10:
+ u = np.cross(plane_normal, [0, 1, 0])
+ u /= np.linalg.norm(u)
+ v = np.cross(plane_normal, u)
+ v /= np.linalg.norm(v)
+
+ # Convert 3D points to 2D using dot products
+ return np.column_stack([points.dot(u), points.dot(v)])
+
+
+def ensure_correct_order(points):
+ """
+ Ensures the points are in counter-clockwise order.
+ If not, it reverses them.
+ """
+ # Calculate signed area
+ n = len(points)
+ area = sum((points[i][0] * points[(i+1)%n][1]) - (points[(i+1)%n][0] * points[i][1]) for i in range(n)) / 2.0
+ # Return the points in reverse order if area is negative
+ return points[::-1] if area < 0 else points
+
+
+
+def sample_random_point(polygon):
+ """
+ Sample a random point from inside the given Shapely polygon.
+ """
+ minx, miny, maxx, maxy = polygon.bounds
+ while True:
+ p = Point(random.uniform(minx, maxx), random.uniform(miny, maxy))
+ if polygon.contains(p):
+ return p
+
+def delete_obj(scene, a, delete_blender=True):
+ if isinstance(a, str):
+ a = [a]
+ if delete_blender:
+ obj_list = [bpy.data.objects[obj_name] for obj_name in a]
+ butil.delete(obj_list)
+ for obj_name in a:
+ # bpy.data.objects.remove(bpy.data.objects[obj_name], do_unlink=True)
+ if scene:
+ scene.graph.transforms.remove_node(obj_name)
+ scene.delete_geometry(obj_name + '_mesh')
+
+
+def global_vertex_coordinates(obj, local_vertex) -> Vector:
+ return obj.matrix_world @ local_vertex.co
+
+def global_polygon_normal(obj, polygon):
+ loc, rot, scale = obj.matrix_world.decompose()
+ rot = rot.to_matrix()
+ normal = rot @ polygon.normal
+ return normal / np.linalg.norm(normal)
+
+def is_planar(obj, tolerance=1e-6):
+ if len(obj.data.polygons) != 1:
+ return False
+
+ polygon = obj.data.polygons[0]
+ global_normal = global_polygon_normal(obj, polygon)
+
+ # Take the first vertex as a reference point on the plane
+ ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon.vertices[0]])
+
+ # Check if all vertices lie on the plane defined by the reference vertex and the global normal
+ for vertex in obj.data.vertices:
+ distance = (global_vertex_coordinates(obj, vertex) - ref_vertex).dot(global_normal)
+ if not math.isclose(distance, 0, abs_tol=tolerance):
+ return False
+
+ return True
+
+def planes_parallel(plane_obj_a, plane_obj_b, tolerance=1e-6):
+ if plane_obj_a.type != 'MESH' or plane_obj_b.type != 'MESH':
+ raise ValueError("Both objects should be of type 'MESH'")
+
+ # # Check if the objects are planar
+ # if not is_planar(plane_obj_a) or not is_planar(plane_obj_b):
+ # raise ValueError("One or both objects are not planar")
+
+ global_normal_a = global_polygon_normal(plane_obj_a, plane_obj_a.data.polygons[0])
+ global_normal_b = global_polygon_normal(plane_obj_b, plane_obj_b.data.polygons[0])
+
+ dot_product = global_normal_a.dot(global_normal_b)
+
+ return math.isclose(dot_product, 1, abs_tol=tolerance) or math.isclose(dot_product, -1, abs_tol=tolerance)
+
+
+def distance_to_plane(point, plane_point, plane_normal):
+ """Compute the distance from a point to a plane defined by a point and a normal."""
+ return abs((point - plane_point).dot(plane_normal))
+
+
+def subset(scene: Scene, incl):
+
+ if isinstance(incl, str):
+ incl = [incl]
+
+ objs = []
+ for n in scene.graph.nodes:
+ T, g = scene.graph[n]
+ if g is None:
+ continue
+ otags = scene.geometry[g].metadata['tags']
+ if any(t in incl for t in otags):
+ objs.append(n)
+
+ # assert len(objs) > 0, incl
+
+ return objs
+
+
+def add_object_cached(col,
+ name,
+ col_obj,
+ fcl_obj):
+ geom = fcl_obj
+ o = col_obj
+ # # Add collision object to set
+ if name in col._objs:
+ col._manager.unregisterObject(col._objs[name])
+ col._objs[name] = {'obj': o,
+ 'geom': geom}
+ # # store the name of the geometry
+ col._names[id(geom)] = name
+
+ col._manager.registerObject(o)
+ col._manager.update()
+ return o
+
+def col_from_subset(scene, names, tags=None, bvh_cache=None):
+
+ if isinstance(names, str):
+ names = [names]
+
+
+ if bvh_cache is not None and bvh_caching_config():
+ tag_key = frozenset(tags) if tags is not None else None
+ key = (frozenset(names), tag_key)
+ res = bvh_cache.get(key)
+ if res is not None:
+ return res
+
+ col = trimesh.collision.CollisionManager()
+
+ for name in names:
+ T, g = scene.graph[name]
+ geom = scene.geometry[g]
+ if tags is not None and len(tags) > 0:
+ obj = blender_objs_from_names(name)[0]
+ mask = tagging.tagged_face_mask(obj, tags)
+ if not mask.any():
+ logger.warning(f'{name=} had {mask.sum()=} for {tags=}')
+ continue
+ geom = geom.submesh(np.where(mask), append=True)
+ T = trimesh.transformations.identity_matrix()
+ t = fcl.Transform(T[:3, :3], T[:3, 3])
+ geom.fcl_obj = col._get_fcl_obj(geom)
+ geom.col_obj = fcl.CollisionObject(geom.fcl_obj, t)
+ assert len(geom.faces) == mask.sum()
+ # col.add_object(name, geom, T)
+ add_object_cached(col, name, geom.col_obj, geom.fcl_obj)
+
+ if len(col._objs) == 0:
+ logger.debug(f'{names=} got no objs, returning None')
+ col = None
+
+ if bvh_cache is not None and bvh_caching_config():
+ bvh_cache[key] = col
+
+ return col
+
+def plot_geometry(ax, geom, color = 'blue'):
+ if isinstance(geom, Polygon):
+ x, y = geom.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc=color, ec='black')
+ elif isinstance(geom, MultiPolygon):
+ for sub_geom in geom:
+ x, y = sub_geom.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc=color, ec='black')
+ elif isinstance(geom, LineString):
+ x, y = geom.xy
+ ax.plot(x, y, color=color)
+ elif isinstance(geom, Point):
+ ax.plot(geom.x, geom.y, 'o', color=color)
+
+
+def sync_trimesh(scene: trimesh.Scene, obj_name: str):
+ bpy.context.view_layer.update()
+ blender_obj = bpy.data.objects[obj_name]
+ mesh = meshes_from_names(scene, obj_name)[0]
+ T_old = mesh.current_transform
+ T = np.array(blender_obj.matrix_world)
+ mesh.apply_transform(T @ np.linalg.inv(T_old))
+ mesh.current_transform = np.array(blender_obj.matrix_world)
+ t = fcl.Transform(T[:3, :3], T[:3, 3])
+ mesh.col_obj.setTransform(t)
+
+def translate(scene: trimesh.Scene, a: str, translation):
+ blender_obj = bpy.data.objects[a]
+ blender_obj.location += Vector(translation)
+ if scene:
+ sync_trimesh(scene, a)
+
+
+def rotate(scene: trimesh.Scene, a: str, axis, angle):
+ blender_obj = bpy.data.objects[a]
+
+ rotation_matrix = trimesh.transformations.rotation_matrix(angle, axis)
+ transform_matrix = Matrix(rotation_matrix).to_4x4()
+ loc, rot, scale = blender_obj.matrix_world.decompose()
+ rot = rot.to_matrix().to_4x4()
+ rot = transform_matrix @ rot
+ rot = rot.to_quaternion()
+ blender_obj.matrix_world = Matrix.LocRotScale(loc, rot, scale)
+
+ if scene:
+ sync_trimesh(scene, a)
+
+
+def set_location(scene: trimesh.Scene, obj_name: str, location):
+ blender_mesh = bpy.data.objects[obj_name]
+ blender_mesh.location = location
+ sync_trimesh(scene, obj_name)
+
+
+
+def set_rotation(scene: trimesh.Scene, obj_name: str, rotation):
+ blender_mesh = blender_objs_from_names(obj_name)[0]
+ blender_mesh.rotation_euler = rotation
+ sync_trimesh(scene, obj_name)
+
+
+# for debugging. does not actually find centroid
+def blender_centroid(a):
+ return np.mean([a.matrix_world @ v.co for v in a.data.vertices], axis = 0)
+
+
+def order_objects_by_principal_axis(objects: list[bpy.types.Object]):
+ locations = [obj.location for obj in objects]
+ location_matrix = np.array(locations)
+ pca = PCA(n_components=1)
+ pca.fit(location_matrix)
+ locations_projected = pca.transform(location_matrix)
+ sorted_indices = np.argsort(locations_projected.ravel())
+ return [objects[i] for i in sorted_indices]
+
diff --git a/infinigen/core/constraints/evaluator/__init__.py b/infinigen/core/constraints/evaluator/__init__.py
new file mode 100644
index 000000000..6300f519b
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/__init__.py
@@ -0,0 +1,10 @@
+from .evaluate import (
+ evaluate_problem,
+ evaluate_node,
+ EvalResult
+)
+from .eval_memo import (
+ evict_memo_for_move,
+ evict_memo_for_obj,
+ memo_key,
+)
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/domain_contains.py b/infinigen/core/constraints/evaluator/domain_contains.py
new file mode 100644
index 000000000..f4a8b167a
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/domain_contains.py
@@ -0,0 +1,60 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import bpy
+
+import logging
+from tqdm import tqdm
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+)
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+def domain_contains(
+ dom: r.Domain,
+ state: state_def.State,
+ obj: state_def.ObjectState
+):
+
+ assert isinstance(dom, r.Domain), dom
+ assert isinstance(obj, state_def.ObjectState), obj
+
+ if not t.satisfies(obj.tags, dom.tags):
+ #logger.debug(f"domain_contains failed, {obj} does not satisfy {obj.tags}")
+ return False
+
+ for rel, dom in dom.relations:
+
+ if isinstance(rel, cl.NegatedRelation):
+ if any(
+ relstate.relation.intersects(rel.rel) and
+ domain_contains(dom, state, state.objs[relstate.target_name])
+ for relstate in obj.relations
+ ):
+ #logger.debug(f"domain_contains failed, {obj} satisfies negative {rel} {dom}")
+ return False
+ else:
+ if not any(
+ relstate.relation.intersects(rel) and
+ domain_contains(dom, state, state.objs[relstate.target_name])
+ for relstate in obj.relations
+ ):
+ #logger.debug(f"domain_contains failed, {obj} does not satisfy {rel} {dom}")
+ return False
+
+ return True
+
+def objkeys_in_dom(dom: r.Domain, curr: state_def.State):
+ return [
+ k for k, o in curr.objs.items()
+ if domain_contains(dom, curr, o) and o.active
+ ]
+
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/eval_memo.py b/infinigen/core/constraints/evaluator/eval_memo.py
new file mode 100644
index 000000000..91d0ea0b5
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/eval_memo.py
@@ -0,0 +1,115 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import logging
+
+from infinigen.core.constraints.example_solver.state_def import State, ObjectState
+from infinigen.core.constraints.example_solver import moves
+
+from infinigen.core.constraints import (
+ constraint_language as cl
+)
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+def memo_key(n: cl.Node):
+ match n:
+ case cl.item(var):
+ return var
+ case cl.scene():
+ return cl.scene
+ case _:
+ return id(n)
+
+def evict_memo_for_obj(
+ node: cl.Problem,
+ memo: dict,
+ obj: ObjectState
+):
+
+ recvals = [
+ evict_memo_for_obj(child, memo, obj)
+ for _, child in node.children()
+ ]
+ res = any(recvals)
+
+ match node:
+ case cl.tagged(_, tags):
+ if not t.implies(obj.tags, tags):
+ res = False
+ case cl.scene():
+ res = True
+ case _:
+ pass
+
+ key = memo_key(node)
+ if res and key in memo:
+ del memo[key]
+
+ return res
+
+def reset_bvh_cache(state, filter_name=None):
+
+ '''
+ filter_name: if specified, only get rid of things containing this
+ '''
+
+ static_tags = {t.Semantics.Room, t.Semantics.Cutter}
+
+ def keep_key(k):
+
+ names, tags = k
+
+ if filter_name is not None:
+ obj = state.objs[filter_name].obj
+ return not (obj.name in names)
+
+ for n in names:
+ if n not in state.objs:
+ return False
+ ostate = state.objs[n]
+ if not ostate.tags.intersection(static_tags):
+ return False
+
+ return True
+
+ prev_keys = list(state.bvh_cache.keys())
+ for k in prev_keys:
+ res = keep_key(k)
+ if res:
+ continue
+ del state.bvh_cache[k]
+
+ logger.debug(f'reset_bvh_cache evicted {len(prev_keys) - len(state.bvh_cache)} out of {len(prev_keys)} orig')
+
+def evict_memo_for_move(
+ problem: cl.Problem,
+ state: State,
+ memo: dict,
+ move: moves.Move
+):
+ match move:
+ case (
+ moves.TranslateMove(names) |
+ moves.RotateMove(names) |
+ moves.Addition(names=names) |
+ moves.ReinitPoseMove(names=names) |
+ moves.RelationPlaneChange(names=names) |
+ moves.Resample(names=names)
+ ):
+ for name in names:
+ assert name is not None, move
+ evict_memo_for_obj(problem, memo, state.objs[name])
+ reset_bvh_cache(state, filter_name=name)
+ case moves.Deletion(name):
+ # TODO hack - delete everything since we cant evict for specific obj after it has ben deleted
+ # easily fixable with more work / refactoring
+ for k in list(memo.keys()):
+ del memo[k]
+ reset_bvh_cache(state)
+ case _:
+ raise NotImplementedError(f'Unsure what to evict for {move=}')
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/evaluate.py b/infinigen/core/constraints/evaluator/evaluate.py
new file mode 100644
index 000000000..6c57343ba
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/evaluate.py
@@ -0,0 +1,255 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from typing import Type, Callable
+from dataclasses import dataclass
+import copy
+import logging
+import operator
+import pandas as pd
+
+from infinigen.core.constraints.example_solver.state_def import State
+from infinigen.core.constraints.evaluator import node_impl, eval_memo
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r
+)
+
+logger = logging.getLogger(__name__)
+
+SPECIAL_CASE_NODES = [
+ cl.ForAll,
+ cl.SumOver,
+ cl.MeanOver,
+ cl.item,
+ cl.Problem,
+ cl.scene
+]
+
+gather_funcs = {
+ cl.ForAll: all,
+ cl.SumOver: sum,
+ cl.MeanOver: lambda vs: (sum(vs) / len(vs)) if len(vs) else 0
+}
+
+def _compute_node_val(node: cl.Node, state: State, memo: dict):
+
+ match node:
+ case cl.scene():
+ return set(k for k, v in state.objs.items() if v.active)
+ case cl.ForAll(objs, var, pred) | cl.SumOver(objs, var, pred) | cl.MeanOver(objs, var, pred):
+ assert isinstance(var, str)
+
+ loop_over_objs = evaluate_node(objs, state, memo)
+
+ results = []
+ for o in loop_over_objs:
+ memo_sub = copy.copy(memo)
+ memo_sub[var] = {o}
+ results.append(evaluate_node(pred, state, memo=memo_sub))
+
+ logger.debug(f'{node.__class__.__name__} had {len(results)=}')
+
+ return gather_funcs[node.__class__](results)
+ case cl.item():
+ raise ValueError(
+ f'_compute_node_val encountered undefined variable {node}. {memo.keys()}'
+ )
+ case cl.Node() if node.__class__ in node_impl.node_impls:
+ impl_func = node_impl.node_impls.get(node.__class__)
+ child_vals = {
+ name: evaluate_node(c, state, memo)
+ for name, c in node.children()
+ }
+ kwargs = {}
+ if hasattr(node, 'others_tags'):
+ kwargs['others_tags'] = getattr(node, 'others_tags')
+ return impl_func(node, state, child_vals, **kwargs)
+ case cl.Problem():
+ raise TypeError(f'evaluate_node is invalid for {node}, please use evaluate_problem')
+ case _:
+ raise NotImplementedError(
+ f'Couldnt compute value for {type(node)}, please add it to '
+ f'{node_impl.node_impls.keys()=} or add a specialcase'
+ )
+
+def relevant(
+ node: cl.Node,
+ filter: r.Domain | None
+) -> bool:
+
+ if filter is None:
+ raise ValueError()
+ return True
+
+ if not isinstance(node, cl.Node):
+ raise ValueError(f'{node=}')
+
+ match node:
+
+ case cl.ObjectSetExpression():
+ d = r.constraint_domain(node, finalize_variables=True)
+ assert r.domain_finalized(d), f'{relevant.__name__} encountered unfinalized {d=}'
+ res = d.intersects(filter, require_satisfies_right=True)
+ logger.debug(f'{relevant.__name__} got {res=} for {d=}\n {filter=}')
+ return res
+ case _:
+ return any(relevant(c, filter) for _, c in node.children())
+
+def _viol_count_binop(
+ node: cl.BoolOperatorExpression,
+ lhs,
+ rhs
+) -> int:
+
+ if not isinstance(lhs, int) or not isinstance(rhs, int):
+ satisfied = node.func(lhs, rhs)
+ return 1 if not satisfied else 0
+
+ match node.func:
+ case operator.ge:
+ return max(0, rhs - lhs)
+ case operator.le:
+ return max(0, lhs - rhs)
+ case operator.gt:
+ return max(0, rhs - lhs + 1)
+ case operator.lt:
+ return max(0, lhs - rhs + 1)
+ case _:
+ raise ValueError(f'Unhandled {node.func=}')
+
+
+def viol_count(
+ node: cl.Node,
+ state: State,
+ memo: dict,
+ filter: r.Domain=None
+):
+
+ match node:
+
+ case cl.BoolOperatorExpression(operator.and_, cons) | cl.Problem(cons):
+ res = sum(viol_count(o, state, memo, filter) for o in cons)
+ case cl.in_range(val, low, high):
+
+ val_res = evaluate_node(val, state, memo)
+
+ if val_res < low:
+ res = low - val_res
+ elif val_res > high:
+ res = val_res - high
+ else:
+ res = 0
+
+ if not relevant(val, filter):
+ res = 0
+
+ case cl.BoolOperatorExpression(operator.eq, [lhs, rhs]):
+ res = abs(evaluate_node(lhs, state, memo) - evaluate_node(rhs, state, memo))
+ if not relevant(lhs, filter) and not relevant(rhs, filter):
+ res = 0
+ case cl.ForAll(objs, var, pred):
+ assert isinstance(var, str)
+ viol = 0
+ for o in evaluate_node(objs, state, memo):
+ memo_sub = copy.copy(memo)
+ memo_sub[var] = {o}
+ viol += viol_count(pred, state, memo_sub, filter)
+ res = viol
+ case (
+ cl.BoolOperatorExpression(operator.ge, [lhs, rhs]) |
+ cl.BoolOperatorExpression(operator.le, [rhs, lhs]) |
+ cl.BoolOperatorExpression(operator.gt, [rhs, lhs]) |
+ cl.BoolOperatorExpression(operator.lt, [rhs, lhs])
+ ):
+ if relevant(lhs, filter) or relevant(rhs, filter):
+ l_res = evaluate_node(lhs, state, memo)
+ r_res = evaluate_node(rhs, state, memo)
+ res = _viol_count_binop(node, l_res, r_res)
+ else:
+ res = 0
+
+ case cl.constant(val) if isinstance(val, bool):
+ res = 0 if val else 1
+ case _:
+ raise NotImplementedError(f'{node.__class__.__name__}(...) is not supported for hard constraints. Please use an alternative. Full node was {node}')
+
+ return res
+
+@dataclass
+class ConstraintsViolated:
+ constraints: list[cl.Node]
+
+ def __bool__(self):
+ return False
+
+def evaluate_node(node: cl.Node, state: State, memo=None):
+
+ k = eval_memo.memo_key(node)
+
+ if memo is None:
+ memo = {}
+ elif k in memo:
+ return memo[k]
+ val = _compute_node_val(node, state, memo)
+
+ memo[k] = val
+ logger.debug(f'Evaluated {node.__class__} to {val}')
+
+ return val
+
+@dataclass
+class EvalResult:
+
+ loss_vals: dict[str, float]
+ violations: dict[str, bool]
+
+ def loss(self):
+ return sum(v for v in self.loss_vals.values())
+
+ def viol_count(self):
+ return sum(x for x in self.violations.values())
+
+ def to_df(self) -> pd.DataFrame:
+ keys = set(self.loss_vals.keys()).union(self.violations.keys())
+ return pd.DataFrame.from_dict({
+ k: dict(
+ loss=self.loss_vals.get(k),
+ viol_count=self.violations.get(k)
+ )
+ for k in keys
+ })
+
+
+def evaluate_problem(
+ problem: cl.Problem,
+ state: State,
+ filter: r.Domain = None,
+ memo=None
+):
+
+ logger.debug(
+ f'Evaluating problem {len(problem.constraints)=} {len(problem.score_terms)=}'
+ )
+
+ if memo is None:
+ memo = {}
+
+ scores = {}
+ for name, score_node in problem.score_terms.items():
+ logger.debug(f'Evaluating score for {name=}')
+ scores[name] = evaluate_node(score_node, state, memo)
+
+ violated = {}
+ for name, node in problem.constraints.items():
+ violated[name] = viol_count(node, state, memo, filter=filter)
+ logger.debug(f'Evaluator found {violated[name]} violations for {name=}')
+
+ return EvalResult(
+ loss_vals=scores,
+ violations=violated
+ )
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/indoor_util.py b/infinigen/core/constraints/evaluator/indoor_util.py
new file mode 100644
index 000000000..aa61cdd3f
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/indoor_util.py
@@ -0,0 +1,231 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import bpy
+import trimesh
+from trimesh import Scene
+from shapely import LineString, Point
+import numpy as np
+from typing import Union
+import random
+import math
+import functools
+
+
+def meshes_from_names(scene, names):
+ if isinstance(names, str):
+ names = [names]
+ return [scene.geometry[g] for _, g in (scene.graph[n] for n in names)]
+
+def blender_objs_from_names(names):
+ if isinstance(names, str):
+ names = [names]
+ return [bpy.data.objects[n] for n in names]
+
+def name_from_mesh(scene, mesh):
+ mesh_name = None
+ for name, mesh in scene.geometry.items():
+ if mesh == mesh:
+ mesh_name = name
+ break
+ return mesh_name
+
+def project_to_xy_path2d(mesh: trimesh.Trimesh) -> trimesh.path.Path2D:
+ poly = trimesh.path.polygons.projected(mesh, (0,0,1), (0,0,0))
+ d = trimesh.path.exchange.misc.polygon_to_path(poly)
+ return trimesh.path.Path2D(entities = d['entities'], vertices = d['vertices'])
+
+def project_to_xy_poly(mesh: trimesh.Trimesh):
+ poly = trimesh.path.polygons.projected(mesh, (0,0,1), (0,0,0))
+ return poly
+
+
+def closest_edge_to_point(polygon, point):
+ closest_distance = float('inf')
+ closest_edge = None
+
+ for i, coord in enumerate(polygon.exterior.coords[:-1]):
+ start, end = coord, polygon.exterior.coords[i + 1]
+ line = LineString([start, end])
+ distance = line.distance(point)
+
+ if distance < closest_distance:
+ closest_distance = distance
+ closest_edge = line
+
+ return closest_edge
+
+def compute_outward_normal(line, polygon):
+ dx = line.xy[0][1] - line.xy[0][0] # x1 - x0
+ dy = line.xy[1][1] - line.xy[1][0] # y1 - y0
+
+ # Candidate normal vectors (perpendicular to edge)
+ normal_vector_1 = np.array([dy, -dx])
+ normal_vector_2 = -normal_vector_1
+
+ # Normalize the vectors (optional but recommended for consistency)
+ normal_vector_1 = normal_vector_1 / np.linalg.norm(normal_vector_1)
+ normal_vector_2 = normal_vector_2 / np.linalg.norm(normal_vector_2)
+
+ # Midpoint of the line segment
+ mid_point = line.interpolate(0.5, normalized=True)
+
+ # Move a tiny bit in the direction of the normals to check which points outside
+ test_point_1 = mid_point.coords[0] + 0.01 * normal_vector_1
+ test_point_2 = mid_point.coords[0] + 0.01 * normal_vector_2
+
+ # Return the normal for which the test point lies outside the polygon
+ if polygon.contains(Point(test_point_1)):
+ return normal_vector_2
+ else:
+ return normal_vector_1
+
+
+def get_transformed_axis(scene, obj_name):
+ obj = bpy.data.objects[obj_name]
+ trimesh_mesh = meshes_from_names(scene, obj_name)[0]
+ axis = trimesh_mesh.axis
+ rot_mat = np.array(obj.matrix_world.to_3x3())
+ return rot_mat @ np.array(axis)
+
+
+def set_axis(scene, objs: Union[str, list[str]], canonical_axis):
+ if isinstance(objs, str):
+ objs = [objs]
+ obj_meshes = meshes_from_names(scene, objs)
+ for obj_name, obj in zip(objs, obj_meshes):
+ obj.axis = canonical_axis
+ obj.axis = get_transformed_axis(scene, obj_name)
+
+
+
+
+def get_plane_from_3dmatrix(matrix):
+ """Extract the plane_normal and plane_origin from a transformation matrix."""
+ # The normal of the plane can be extracted from the 3x3 rotation part of the matrix
+ plane_normal = matrix[:3, 2]
+ plane_origin = matrix[:3, 3]
+ return plane_normal, plane_origin
+
+
+def project_points_onto_plane(points, plane_origin, plane_normal):
+ """Project 3D points onto a plane."""
+ d = np.dot(points - plane_origin, plane_normal)[:, None]
+ return points - d * plane_normal
+
+def to_2d_coordinates(points, plane_normal):
+ """Convert 3D points to 2D using the plane defined by its normal."""
+ # Compute two perpendicular vectors on the plane
+ u = np.cross(plane_normal, [1, 0, 0])
+ if np.linalg.norm(u) < 1e-10:
+ u = np.cross(plane_normal, [0, 1, 0])
+ u /= np.linalg.norm(u)
+ v = np.cross(plane_normal, u)
+ v /= np.linalg.norm(v)
+
+ # Convert 3D points to 2D using dot products
+ return np.column_stack([points.dot(u), points.dot(v)])
+
+
+def ensure_correct_order(points):
+ """
+ Ensures the points are in counter-clockwise order.
+ If not, it reverses them.
+ """
+ # Calculate signed area
+ n = len(points)
+ area = sum((points[i][0] * points[(i+1)%n][1]) - (points[(i+1)%n][0] * points[i][1]) for i in range(n)) / 2.0
+ # Return the points in reverse order if area is negative
+ return points[::-1] if area < 0 else points
+
+
+
+def sample_random_point(polygon):
+ """
+ Sample a random point from inside the given Shapely polygon.
+ """
+ minx, miny, maxx, maxy = polygon.bounds
+ while True:
+ p = Point(random.uniform(minx, maxx), random.uniform(miny, maxy))
+ if polygon.contains(p):
+ return p
+
+
+def delete_obj(a, scene = None):
+ if isinstance(a, str):
+ a = [a]
+ for obj_name in a:
+ bpy.data.objects.remove(bpy.data.objects[obj_name], do_unlink=True)
+ if scene:
+ scene.graph.transforms.remove_node(obj_name)
+ scene.delete_geometry(obj_name + '_mesh')
+
+
+def global_vertex_coordinates(obj, local_vertex):
+ return obj.matrix_world @ local_vertex.co
+
+def global_polygon_normal(obj, polygon):
+ loc, rot, scale = obj.matrix_world.decompose()
+ rot = rot.to_matrix()
+ normal = rot @ polygon.normal
+ return normal / np.linalg.norm(normal)
+
+def is_planar(obj, tolerance=1e-6):
+ if len(obj.data.polygons) != 1:
+ return False
+
+ polygon = obj.data.polygons[0]
+ global_normal = global_polygon_normal(obj, polygon)
+
+ # Take the first vertex as a reference point on the plane
+ ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon.vertices[0]])
+
+ # Check if all vertices lie on the plane defined by the reference vertex and the global normal
+ for vertex in obj.data.vertices:
+ distance = (global_vertex_coordinates(obj, vertex) - ref_vertex).dot(global_normal)
+ if not math.isclose(distance, 0, abs_tol=tolerance):
+ return False
+
+ return True
+
+def planes_parallel(plane_obj_a, plane_obj_b, tolerance=1e-6):
+ if plane_obj_a.type != 'MESH' or plane_obj_b.type != 'MESH':
+ raise ValueError("Both objects should be of type 'MESH'")
+
+ # # Check if the objects are planar
+ # if not is_planar(plane_obj_a) or not is_planar(plane_obj_b):
+ # raise ValueError("One or both objects are not planar")
+
+ global_normal_a = global_polygon_normal(plane_obj_a, plane_obj_a.data.polygons[0])
+ global_normal_b = global_polygon_normal(plane_obj_b, plane_obj_b.data.polygons[0])
+
+ dot_product = global_normal_a.dot(global_normal_b)
+
+ return math.isclose(dot_product, 1, abs_tol=tolerance) or math.isclose(dot_product, -1, abs_tol=tolerance)
+
+
+def distance_to_plane(point, plane_point, plane_normal):
+ """Compute the distance from a point to a plane defined by a point and a normal."""
+ return abs((point - plane_point).dot(plane_normal))
+
+def is_within_margin_from_plane(obj, obj_b, margin, tol = 1e-6):
+ """Check if all vertices of an object are within a given margin from a plane."""
+ polygon_b = obj_b.data.polygons[0]
+ plane_point_b = global_vertex_coordinates(obj_b, obj_b.data.vertices[polygon_b.vertices[0]])
+ plane_normal_b = global_polygon_normal(obj_b, polygon_b)
+ for vertex in obj.data.vertices:
+ global_vertex = global_vertex_coordinates(obj, vertex)
+ distance = distance_to_plane(global_vertex, plane_point_b, plane_normal_b)
+ if not math.isclose(distance, margin, abs_tol=tol):
+ return False
+ return True
+
+# def update_blender_representation(scene, trimesh_obj):
+
+# transform_matrix =
+
+
+# def update_trimesh_representation(scnene, blender_obj):
+# pass
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/node_impl/__init__.py b/infinigen/core/constraints/evaluator/node_impl/__init__.py
new file mode 100644
index 000000000..138875f46
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/node_impl/__init__.py
@@ -0,0 +1 @@
+from .impl_bindings import node_impls
\ No newline at end of file
diff --git a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py
new file mode 100644
index 000000000..3ee9e0f04
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py
@@ -0,0 +1,374 @@
+# Copyright (c) Princeton University.
+
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Karhan Kayan: geometry impl bindings
+# - Alexander Raistrick: impl interface, set_reasoning / operator impls
+# - Lingjie Mei: bugfix
+
+import math
+import logging
+import functools
+
+import gin
+import numpy as np
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r
+)
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core import tags as t
+from infinigen.core.constraints.evaluator import domain_contains
+
+from . import trimesh_geometry, symmetry
+import inspect
+
+logger = logging.getLogger(__name__)
+
+node_impls = {}
+
+def statenames_to_blnames(state, names):
+ return [state.objs[n].obj.name for n in names]
+
+def register_node_impl(node_cls):
+ def decorator(func):
+ node_impls[node_cls] = func
+ return func
+ return decorator
+
+def generic_impl_interface(
+ cons: cl.Node,
+ state: state_def.State,
+ child_vals: dict
+):
+ pass
+
+@register_node_impl(cl.constant)
+def constant_impl(
+ cons: cl.Node,
+ state: state_def.State,
+ child_vals: dict
+):
+ return cons.value
+
+@register_node_impl(cl.ScalarOperatorExpression)
+@register_node_impl(cl.BoolOperatorExpression)
+def operator_impl(
+ cons: cl.ScalarOperatorExpression | cl.BoolOperatorExpression,
+ state: state_def.State,
+ child_vals: dict
+):
+
+ operands = [
+ child_vals[f'operands[{i}]']
+ for i in range(len(cons.operands))
+ ]
+
+ try:
+ if (
+ isinstance(cons.func, np.ufunc)
+ or len(operands) == 1
+ ):
+ return cons.func(*operands)
+ else:
+ return functools.reduce(cons.func, operands)
+ except ZeroDivisionError as e:
+ raise ZeroDivisionError(f'{e} in {cons=}, {operands=}')
+
+@register_node_impl(cl.center_stable_surface_dist)
+def center_stable_surface_impl(
+ cons: cl.center_stable_surface_dist,
+ state: state_def.State,
+ child_vals: dict
+):
+ objs = child_vals['objs']
+ return trimesh_geometry.center_stable_surface(state.trimesh_scene, objs, state)
+
+@register_node_impl(cl.accessibility_cost)
+def accessibility_impl(
+ cons: cl.accessibility_cost,
+ state: state_def.State,
+ child_vals: dict,
+ use_collision_impl: bool = True
+):
+
+ objs = statenames_to_blnames(state, child_vals['objs'])
+ others = statenames_to_blnames(state, child_vals['others'])
+ if len(objs) == 0:
+ return 0
+
+ if use_collision_impl:
+ res = trimesh_geometry.accessibility_cost_cuboid_penetration(
+ state.trimesh_scene,
+ objs,
+ others,
+ cons.normal,
+ cons.dist,
+ bvh_cache=state.bvh_cache
+ )
+ else:
+ res = trimesh_geometry.accessibility_cost(
+ state.trimesh_scene, objs, others, cons.normal
+ )
+ return res
+
+@register_node_impl(cl.distance)
+def min_distance_impl(
+ cons: cl.Node,
+ state: state_def.State,
+ child_vals: dict,
+ others_tags: set = None
+):
+
+ objs = statenames_to_blnames(state, child_vals['objs'])
+ others = statenames_to_blnames(state, child_vals['others'])
+
+ if len(objs) == 0 or len(others) == 0:
+ logger.debug('min_distance had no targets')
+ return 0
+
+ res = trimesh_geometry.min_dist(
+ state.trimesh_scene,
+ a=objs,
+ b=others,
+ b_tags=others_tags,
+ bvh_cache=state.bvh_cache
+ )
+
+ if res.dist < 0:
+ return 0
+
+ return res.dist
+
+@register_node_impl(cl.min_distance_internal)
+def min_distance_internal_impl(
+ cons: cl.min_distance_internal,
+ state: state_def.State,
+ child_vals: dict
+):
+ objs = statenames_to_blnames(state, child_vals['objs'])
+ if len(objs) <= 1:
+ return 0
+ return trimesh_geometry.min_dist(
+ state.trimesh_scene, a=objs
+ ).dist
+
+@register_node_impl(cl.min_dist_2d)
+def min_dist_2d_impl(
+ cons: cl.min_dist_2d,
+ state: state_def.State,
+ child_vals: dict
+):
+ a = statenames_to_blnames(state, child_vals['objs'])
+ b = statenames_to_blnames(state, child_vals['others'])
+ if len(a) == 0 or len(b) == 0:
+ return 0
+ return trimesh_geometry.min_dist_2d(
+ state.trimesh_scene, a, b
+ )
+
+@register_node_impl(cl.focus_score)
+def focus_score_impl(
+ cons: cl.focus_score,
+ state: state_def.State,
+ child_vals: dict,
+):
+
+ a = statenames_to_blnames(state, child_vals['objs'])
+ b = statenames_to_blnames(state, child_vals['others'])
+
+ if len(a) == 0 or len(b) == 0:
+ return 0
+
+ return trimesh_geometry.focus_score(
+ state,
+ a=a,
+ b=b
+ )
+
+@register_node_impl(cl.angle_alignment_cost)
+def angle_alignment_impl(
+ cons: cl.angle_alignment_cost,
+ state: state_def.State,
+ child_vals: dict,
+ others_tags: set = None
+):
+ a = statenames_to_blnames(state, child_vals['objs'])
+ b = statenames_to_blnames(state, child_vals['others'])
+ if len(a) == 0 or len(b) == 0:
+ return 0
+ return trimesh_geometry.angle_alignment_cost(
+ state, a, b, others_tags
+ )
+
+@register_node_impl(cl.freespace_2d)
+def freespace_2d_impl(
+ cons: cl.freespace_2d,
+ state: state_def.State,
+ child_vals: dict
+):
+ return trimesh_geometry.freespace_2d()
+
+@register_node_impl(cl.rotational_asymmetry)
+def rotational_asymmetry_impl(
+ cons: cl.rotational_asymmetry,
+ state: state_def.State,
+ child_vals: dict
+):
+ objs = statenames_to_blnames(state, child_vals['objs'])
+ if len(objs) <= 1:
+ return 0
+ return symmetry.compute_total_rotation_asymmetry(objs)
+
+@register_node_impl(cl.reflectional_asymmetry)
+def reflectional_asymmetry_impl(
+ cons: cl.reflectional_asymmetry,
+ state: state_def.State,
+ child_vals: dict,
+ use_long_plane: bool = True,
+):
+
+ objs = statenames_to_blnames(state, child_vals['objs'])
+ others = statenames_to_blnames(state, child_vals['others'])
+ if len(objs) <= 1:
+ return 0
+ return trimesh_geometry.reflectional_asymmetry_score(
+ state.trimesh_scene, objs, others, use_long_plane
+ )
+
+@register_node_impl(cl.coplanarity_cost)
+def coplanarity_cost_impl(
+ cons: cl.coplanarity_cost,
+ state: state_def.State,
+ child_vals: dict
+):
+ objs = child_vals['objs']
+ if len(objs) <= 1:
+ return 0
+ return trimesh_geometry.coplanarity_cost(state.trimesh_scene, objs)
+
+
+@register_node_impl(cl.tagged)
+def tagged_impl(
+ cons: cl.tagged,
+ state: state_def.State,
+ child_vals: dict
+):
+ res = {
+ o for o in child_vals['objs']
+ if t.satisfies(state.objs[o].tags, cons.tags)
+ }
+
+ #logger.debug('tagged(%s) produced %s from %i candidates', cons.tags, res, len(child_vals['objs']))
+
+ return res
+
+@register_node_impl(cl.count)
+def count_impl(
+ cons: cl.count,
+ state: state_def.State,
+ child_vals: dict
+):
+ return len(child_vals['objs'])
+
+@register_node_impl(cl.in_range)
+def in_range_impl(
+ cons: cl.in_range,
+ state: state_def.State,
+ child_vals: dict
+):
+ x = child_vals['val']
+ return (
+ x <= cons.high and
+ x >= cons.low
+ )
+
+@register_node_impl(cl.related_to)
+def related_children_impl(
+ cons: cl.related_to,
+ state: state_def.State,
+ child_vals: dict
+):
+
+ r = cons.relation
+ children: set[str] = child_vals['child']
+ parents: set[str] = child_vals['parent']
+
+ res = set()
+ for o in children:
+ if any(
+ rs.relation.implies(r) and rs.target_name in parents
+ for rs in state.objs[o].relations
+ ):
+ res.add(o)
+
+ #logger.debug('related_to %s produced %s from %i candidates', cons.relation, res, len(children))
+
+ return res
+
+@register_node_impl(cl.excludes)
+def excludes_impl(
+ cons: cl.excludes,
+ state: state_def.State,
+ child_vals: dict
+):
+
+ return {
+ o for o in child_vals['objs']
+ if state.objs[o].tags.isdisjoint(cons.tags)
+ }
+
+@register_node_impl(cl.volume)
+def volume_impl(
+ cons: cl.volume,
+ state: state_def.State,
+ child_vals: dict
+):
+ objs = child_vals['objs']
+
+ res = 0
+ for o in objs:
+
+ s = state.objs[o]
+ dims = sorted(list(s.obj.dimensions), reverse=True)
+
+ if isinstance(cons.dims, int):
+ dims = dims[:cons.dims]
+ elif isinstance(cons.dims, tuple):
+ dims = np.array(dims)[np.array(cons.dims)]
+ else:
+ raise TypeError(f'Unexpected {type(cons.dims)=}')
+
+ res += math.prod(dims)
+
+ return res
+
+@register_node_impl(cl.hinge)
+def hinge_impl(
+ cons: cl.hinge,
+ state: state_def.State,
+ child_vals: dict
+):
+ x = child_vals['val']
+
+ if x < cons.low:
+ return cons.low - x
+ elif x > cons.high:
+ return x - cons.high
+ else:
+ return 0
+
+@register_node_impl(r.FilterByDomain)
+def filter_by_domain_impl(
+ cons: r.FilterByDomain,
+ state: state_def.State,
+ child_vals: dict
+) -> set[str]:
+
+ return {
+ o
+ for o in child_vals['objs']
+ if domain_contains.domain_contains(cons.filter, state, state.objs[o])
+ }
diff --git a/infinigen/core/constraints/evaluator/node_impl/symmetry.py b/infinigen/core/constraints/evaluator/node_impl/symmetry.py
new file mode 100644
index 000000000..ec12f6017
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/node_impl/symmetry.py
@@ -0,0 +1,263 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+# Acknowledgement: Rotational symmetry code draws inspiration from https://pubs.acs.org/doi/abs/10.1021/ja00046a033 by Zabrodsky et al.
+
+
+import numpy as np
+from typing import Union, Any
+from infinigen.core.constraints.evaluator.indoor_util import blender_objs_from_names
+from mathutils import Vector, Quaternion, Matrix
+from scipy.optimize import linear_sum_assignment
+import matplotlib.pyplot as plt
+
+
+def rotate_vector(vector, angle):
+ """Rotate a 2D vector by a given angle."""
+ rotation_matrix = np.array([
+ [np.cos(angle), -np.sin(angle)],
+ [np.sin(angle), np.cos(angle)]
+ ])
+ return np.dot(vector, rotation_matrix.T)
+
+def compute_centroid(objects):
+ """Compute the centroid of the provided objects."""
+ total_coords = np.zeros(2)
+ for obj in objects:
+ total_coords += np.array([obj.location.x, obj.location.y])
+ return total_coords / len(objects)
+
+def compute_location_asymmetry(objects, centroid):
+ """Compute location asymmetry based on the described method."""
+ num_objects = len(objects)
+
+ P = np.zeros((num_objects, 2))
+ for i, obj in enumerate(objects):
+ P[i] = np.array([obj.location.x, obj.location.y]) - centroid
+ # print(P)
+
+ # 2. Rotate all P_i so that P_1 is aligned with the x axis
+ angle_p1 = np.arctan2(P[0][1], P[0][0])
+ for i in range(num_objects):
+ P[i] = rotate_vector(P[i], -angle_p1)
+
+
+ # 3. Normalize P_i by dividing by max norm
+ max_norm = max(np.linalg.norm(P, axis=1))
+ P /= max_norm
+ # print(P)
+
+
+ # 4. Compute Q as the average of the rotated P_i vectors
+ Q = np.zeros(2)
+ for i in range(num_objects):
+ rotated_p = rotate_vector(P[i], -i * 2 * np.pi / num_objects)
+# print("rot", P[i], rotated_p)
+ Q += rotated_p
+ Q /= num_objects
+
+ # 5. and 6. Compute Q_i and find the MSD between Q_i and P_i
+ total_msd = 0
+ for i in range(num_objects):
+ Q_i = rotate_vector(Q, i * 2 * np.pi / num_objects)
+# print("rot2", Q_i, P[i])
+ msd = np.linalg.norm(Q_i - P[i])**2
+ total_msd += msd
+
+ return total_msd / num_objects
+
+
+def compute_orientation_asymmetry(objects, centroid):
+ """Compute orientation asymmetry of objects."""
+ num_objects = len(objects)
+
+ # 1. Get the orientation vectors
+ V = np.zeros((num_objects, 2))
+ for i, obj in enumerate(objects):
+ # Extract orientation from object's rotation attribute
+ V[i] = np.array([np.cos(obj.rotation_euler.z), np.sin(obj.rotation_euler.z)])
+
+
+ # Rotate all V_i so that V_1 is aligned with the x axis
+ angle_v1 = np.arctan2(V[0][1], V[0][0])
+ for i in range(num_objects):
+ V[i] = rotate_vector(V[i], -angle_v1)
+
+
+ # Normalize V_i by dividing by max norm
+ max_norm = max(np.linalg.norm(V, axis=1))
+ V /= max_norm
+
+ # 4. Compute Q as the average of the rotated V_i vectors
+ Q = np.zeros(2)
+ for i in range(num_objects):
+ rotated_v = rotate_vector(V[i], -i * 2 * np.pi / num_objects)
+# print("rot", P[i], rotated_p)
+ Q += rotated_v
+ Q /= num_objects
+
+
+ # 5. and 6. Compute Q_i and find the MSD between Q_i and V_i
+ total_msd = 0
+ for i in range(num_objects):
+ Q_i = rotate_vector(Q, i * 2 * np.pi / num_objects)
+# print("rot2", Q_i, P[i])
+ msd = np.linalg.norm(Q_i - V[i])**2
+ total_msd += msd
+
+ return total_msd / num_objects
+
+def sort_objects_clockwise(objects, centroid):
+ angles = []
+ for obj in objects:
+ # Calculate the angle from the centroid to the object
+ dx = obj.location.x - centroid[0]
+ dy = obj.location.y - centroid[1]
+ angle = np.arctan2(dy, dx)
+ angles.append((obj, angle))
+
+ # Sort objects based on the angles in descending order for clockwise sorting
+ angles.sort(key=lambda x: x[1])
+
+ # Extract the sorted objects
+ sorted_objects = [obj for obj, angle in angles]
+ return sorted_objects
+
+
+def compute_total_rotation_asymmetry(a: Union[str, list[str]]) -> float:
+ """Compute the total asymmetry."""
+ if isinstance(a, str):
+ a = [a]
+ objects = blender_objs_from_names(a)
+ centroid = compute_centroid(objects)
+ objects = sort_objects_clockwise(objects, centroid)
+ location_asymmetry = compute_location_asymmetry(objects, centroid)
+ orientation_asymmetry = compute_orientation_asymmetry(objects, centroid)
+
+ # print("location asym", location_asymmetry, "orient asym", orientation_asymmetry)
+
+ return (location_asymmetry + orientation_asymmetry) / 2
+
+
+
+def reflect_point(point, plane_point, plane_normal):
+ # Reflect a point across an arbitrary plane defined by a point and a normal.
+ to_point = point - plane_point
+ distance_to_plane = to_point.dot(plane_normal)
+ reflected_point = point - 2 * distance_to_plane * plane_normal
+ return reflected_point
+
+# prob doesnt work
+def reflect_quaternion(q, n):
+ # Decompose the quaternion into scalar and vector parts
+ w = q.w
+ v = Vector((q.x, q.y, q.z))
+
+ # Reflect the vector part
+ v_reflected = v - 2 * v.dot(n) * n
+
+ # Construct the reflected quaternion
+ q_reflected = Quaternion((w, v_reflected.x, v_reflected.y, v_reflected.z))
+
+ return q_reflected
+
+
+def reflect_axis_angle(axis_angle, n):
+ axis = Vector((axis_angle[1], axis_angle[2], axis_angle[3]))
+ angle = axis_angle[0]
+ # Reflect the vector part
+ v_reflected = axis - 2 * axis.dot(n) * n
+ angle_reflected = -angle
+
+ # Construct the reflected axis angle
+ axis_angle_reflected = Vector((angle_reflected, v_reflected.x, v_reflected.y, v_reflected.z))
+
+ return axis_angle_reflected
+
+def reflect(obj, plane_point, plane_normal):
+ obj.rotation_mode = 'AXIS_ANGLE'
+ reflected_position = reflect_point(obj.location, plane_point, plane_normal)
+ reflected_axis_angle = reflect_axis_angle(obj.rotation_axis_angle, plane_normal)
+ reflected_quaternion = Matrix.Rotation(reflected_axis_angle[0], 4, reflected_axis_angle[1:]).to_quaternion()
+ return reflected_position, reflected_quaternion
+
+
+def distance(pos1, pos2):
+ # Calculate Euclidean distance between two positions
+ return (pos1 - pos2).length
+
+def angle_difference(orient1, orient2):
+ # Calculate the angular difference between two orientations represented as quaternions.
+ orient1.normalize()
+ orient2.normalize()
+ dot_product = orient1.dot(orient2)
+ # lose directionality information
+ dot_product = abs(dot_product)
+ dot_product = max(min(dot_product, 1.0), -1.0)
+ angle = 2 * np.arccos(dot_product)
+ return angle
+
+def weight(obj):
+ # Assign a weight based on obj size or other criteria
+ bbox = obj.bound_box
+ dims = [bbox[i][0] for i in range(8)]
+ volume = (max(dims) - min(dims)) ** 3
+ return volume
+
+def normalization_factor(objs):
+ avg_distance = np.mean([distance(obj1.location, obj2.location) for obj1 in objs for obj2 in objs if obj1 != obj2])
+ return avg_distance
+
+def bipartite_matching(objs, reflected_objs_data):
+ # Use the Hungarian algorithm to find the optimal pairing between objs and reflected_objs
+ for obj in objs:
+ obj.rotation_mode = 'QUATERNION'
+ cost_matrix = np.array([[distance(obj.location, ref[0]) + angle_difference(obj.rotation_quaternion, ref[1]) for ref in reflected_objs_data] for obj in objs])
+ row_ind, col_ind = linear_sum_assignment(cost_matrix)
+ return [(objs[i], reflected_objs_data[j]) for i, j in zip(row_ind, col_ind)]
+
+
+def calculate_reflectional_asymmetry(objs, plane_point, plane_normal, visualize = False):
+ if visualize:
+ fig, ax = plt.subplots()
+ # plot plane point and plane normal
+ ax.scatter(plane_point.x, plane_point.y, c='g', label='plane point')
+ ax.quiver(plane_point.x, plane_point.y, plane_normal.x, plane_normal.y, color='g', label='plane normal')
+
+
+ reflected_objs_data = [reflect(obj, plane_point, plane_normal) for obj in objs]
+
+ # Use bipartite matching to find optimal pairings
+ pairings = bipartite_matching(objs, reflected_objs_data)
+
+ total_deviation = 0
+ for original, reflected_data in pairings:
+ positional_deviation = distance(original.location, reflected_data[0])
+ original.rotation_mode = 'QUATERNION'
+ angular_deviation = angle_difference(original.rotation_quaternion, reflected_data[1])
+
+ weighted_deviation = weight(original) * (positional_deviation + angular_deviation)
+ total_deviation += weighted_deviation
+ if visualize:
+ # plot the point and the reflected point with different colors
+ ax.scatter(original.location.x, original.location.y, c='b', label='original point')
+ ax.scatter(reflected_data[0].x, reflected_data[0].y, c='r', label='reflected point')
+
+
+ # Normalize based on scene scale or other criteria
+ normalized_deviation = total_deviation / normalization_factor(objs)
+
+ symmetry_score = 1 / (1 + normalized_deviation)
+ asymmetry_score = 1 - symmetry_score
+
+ for obj in objs:
+ obj.rotation_mode = 'XYZ'
+
+ if visualize:
+ ax.legend()
+ plt.show()
+
+
+ return asymmetry_score
+
diff --git a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py
new file mode 100644
index 000000000..abdf935c0
--- /dev/null
+++ b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py
@@ -0,0 +1,1172 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Karhan Kayan: primary author
+# - Alexander Raistrick: initial version of collision/distance
+# Acknowledgement: Some metrics draw inspiration from https://dl.acm.org/doi/10.1145/1964921.1964981 by Yu et al.
+
+from __future__ import annotations
+from typing import Union, Any
+import logging
+from dataclasses import dataclass
+from copy import copy
+
+import numpy as np
+import gin
+
+import bpy
+import trimesh
+
+from trimesh import Trimesh, Scene
+import networkx as nx
+from shapely.geometry import Point, LineString
+from shapely.ops import unary_union, nearest_points
+from shapely import Polygon
+from shapely import MultiPolygon
+
+import matplotlib.pyplot as plt
+# import fcl
+
+# from infinigen.core.util import blender as butil
+from infinigen.core.util import blender as butil
+from mathutils import Vector, Quaternion
+from scipy.optimize import linear_sum_assignment
+
+
+
+import infinigen.core.constraints.constraint_language.util as iu
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core.constraints.example_solver.geometry.parse_scene import add_to_scene
+
+from infinigen.core import tags as t, tagging
+import infinigen.core.constraints.evaluator.node_impl.symmetry as symmetry
+
+
+# from infinigen.core.tagging import tag_object,tag_system
+# from scipy.optimize import dual_annealing
+# from tqdm import tqdm
+
+logger = logging.getLogger(__name__)
+
+def get_cardinal_planes_bbox(vertices: np.ndarray):
+ """
+ Get the mid dividing planes. Assumes vertices form a box
+ """
+ centroid = np.mean(vertices, axis=0)
+
+ # Calculate the covariance matrix and principal components
+ centered_vertices = vertices - centroid
+ cov_matrix = np.cov(centered_vertices[:,:2].T) # Covariance on XY plane
+ eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
+
+ # Sort eigenvectors based on eigenvalues
+ order = eigenvalues.argsort()[::-1]
+ principal_axes = eigenvectors[:, order]
+
+ # Determine the longer and shorter plane normals and normalize them
+ if eigenvalues[order[0]] > eigenvalues[order[1]]:
+ longer_plane_normal = np.array([principal_axes[0, 1], principal_axes[1, 1], 0])
+ shorter_plane_normal = np.array([principal_axes[0, 0], principal_axes[1, 0], 0])
+ else:
+ longer_plane_normal = np.array([principal_axes[0, 0], principal_axes[1, 0], 0])
+ shorter_plane_normal = np.array([principal_axes[0, 1], principal_axes[1, 1], 0])
+
+ longer_plane_normal /= np.linalg.norm(longer_plane_normal)
+ shorter_plane_normal /= np.linalg.norm(shorter_plane_normal)
+ return [[Vector(centroid), Vector(longer_plane_normal)], [Vector(centroid), Vector(shorter_plane_normal)]]
+
+def get_axis(state: state_def.State, obj: bpy.types.Object, tag = t.Subpart.Front):
+
+ a_front_planes = state.planes.get_tagged_planes(obj, tag)
+ if len(a_front_planes) > 1:
+ logging.warning(f'{obj.name=} had too many front planes ({len(a_front_planes)})')
+ a_front_plane = a_front_planes[0]
+ a_front_plane_ind = a_front_plane[1]
+ a_poly = obj.data.polygons[a_front_plane_ind]
+ front_plane_pt = iu.global_vertex_coordinates(obj, obj.data.vertices[a_poly.vertices[0]])
+ front_plane_normal = iu.global_polygon_normal(obj, a_poly)
+ return front_plane_pt, front_plane_normal
+
+def preprocess_collision_query_cases(a, b, a_tags, b_tags):
+
+ if isinstance(a, list):
+ a = set(a)
+ if isinstance(b, list):
+ b = set(b)
+
+ if len(a) == 1:
+ a = a.pop()
+ if len(b) == 1:
+ b = b.pop()
+
+ # eliminate symmetrical cases
+ if (
+ a is None or
+ (isinstance(b, set) and not isinstance(a, set))
+ ):
+ a, b = b, a
+ a_tags, b_tags = b_tags, a_tags
+
+ # nobody wants to be told a 0 distance if they query how far a chair is from the set of all chairs
+ if isinstance(b, str) and isinstance(a, set) and b in a:
+ a.remove(b)
+
+ if isinstance(a, set) and len(a) == 0:
+ raise ValueError(f'query recieved empty input {a=}')
+ if isinstance(a, set) and len(a) == 0:
+ raise ValueError(f'query recieved empty input {b=}')
+
+ # single-to-single is treated as many-to-single
+ if isinstance(a, str):
+ a = [a]
+
+ assert a is not None
+
+ if a_tags is None:
+ a_tags = set()
+ if b_tags is None:
+ b_tags = set()
+
+ return a, b, a_tags, b_tags
+
+@dataclass
+class ContactResult:
+ hit: bool
+ names: list[str]
+ contacts: list
+
+def any_touching(
+ scene: Scene,
+ a: Union[str, list[str]],
+ b: Union[str, list[str]] = None,
+ a_tags=None,
+ b_tags=None,
+ bvh_cache=None
+):
+
+ '''
+ Computes one-to-one, many-to-one, one-to-many or many-to-many collisions
+
+ In all cases, returns True if any one object from a and b touch
+ '''
+ a, b, a_tags, b_tags = preprocess_collision_query_cases(a, b, a_tags, b_tags)
+
+ col = iu.col_from_subset(scene, a, a_tags, bvh_cache)
+
+ if b is None and len(a) == 1:
+ # query makes no sense, asking for intra-set collision on one element
+ hit, names, contacts = None, (a, b), []
+ elif b is None:
+ hit, names, contacts = col.in_collision_internal(return_data=True, return_names=True)
+ elif isinstance(b, str):
+ T, g = scene.graph[b]
+ hit, names, contacts = col.in_collision_single(scene.geometry[g], transform=T, return_data=True, return_names=True)
+ elif isinstance(b, list):
+ col2 = iu.col_from_subset(scene, b, b_tags, bvh_cache)
+ hit, names, contacts = col.in_collision_other(col2, return_names=True, return_data=True)
+ else:
+ raise ValueError(f'Unhandled case {a=} {b=}')
+
+ names = list(names)
+ if len(names) == 1:
+ assert isinstance(b, str)
+ names.append(b)
+ logging.debug(f'added name {b} to make {names}')
+
+ if len(names) == 0:
+ names = [a, b]
+
+ return ContactResult(
+ hit=hit,
+ names=names,
+ contacts=contacts
+ )
+
+@dataclass
+class DistanceResult:
+ dist: float
+ names: list[str]
+ data: trimesh.collision.DistanceData
+
+def min_dist(
+ scene: Scene,
+ a: Union[str, list[str]],
+ b: Union[str, list[str]] = None,
+ a_tags: set = None,
+ b_tags: set = None,
+ bvh_cache: dict = None
+):
+
+ '''
+ Computes one-to-one, many-to-one, one-to-many or many-to-many distance
+
+ In all cases, returns the minimum distance between any object in a and b
+ '''
+ # we get fcl error otherwise
+ if len(a) == 1 and len(b) == 1 and a[0] == b[0]:
+ return DistanceResult(dist=0, names=[a[0], b[0]], data=None)
+ a, b, a_tags, b_tags = preprocess_collision_query_cases(a, b, a_tags, b_tags)
+ col = iu.col_from_subset(scene, a, a_tags, bvh_cache)
+
+
+
+ if b is None and len(a) == 1:
+ dist, data = 1e9, None
+ elif b is None:
+ dist, data = col.min_distance_internal(return_data=True)
+ elif isinstance(b, str):
+ T, g = scene.graph[b]
+ geom = scene.geometry[g]
+ if b_tags is not None and len(b_tags) > 0:
+ obj = iu.blender_objs_from_names(b)[0]
+ mask = tagging.tagged_face_mask(obj, b_tags)
+ if not mask.any():
+ logger.warning(f'{b=} had {mask.sum()=} for {b_tags=}')
+ geom = geom.submesh(np.where(mask), append=True)
+ assert len(geom.faces) == mask.sum()
+ dist, data = col.min_distance_single(geom, transform=T, return_data=True)
+ if '__external' in data.names:
+ data.names.remove('__external')
+ data.names.add(b)
+ data._points[b] = data._points['__external']
+ data._points.pop('__external')
+ logging.debug(f"WARNING: swapped __external for {b} to make {data.names}")
+ elif isinstance(b, (list, set)):
+ col2 = iu.col_from_subset(scene, b, b_tags, bvh_cache)
+ dist, data = col.min_distance_other(col2, return_data=True)
+ else:
+ raise ValueError(f'Unhandled case {a=} {b=}')
+
+ if data is not None:
+ assert '__external' not in data.names
+
+ return DistanceResult(
+ dist=dist,
+ names=list(data.names) if data is not None else None,
+ data=data
+ )
+
+def contains(
+ scene: Scene,
+ a: str,
+ b: str,
+ tol = 1e-6
+) -> bool:
+ """
+ Check if a contains b
+ """
+ mesh_a = scene.geometry[a]
+ mesh_b = scene.geometry[b]
+
+
+ difference = mesh_a.difference(mesh_b)
+
+ return abs(difference.volume - mesh_a.volume) < tol
+
+def contains_all(scene: trimesh.Scene, a: Union[str, list[str]], b: Union[str, list[str]]) -> bool:
+ """
+ Check if all objects in list 'a' contain all objects in list 'b' within the given scene.
+
+ Parameters:
+ - scene: The trimesh.Scene instance.
+ - a: Name or list of names of objects to check for containment.
+ - b: Name or list of names of objects that might be contained.
+
+ Returns:
+ - True if all objects in list 'a' contain all objects in list 'b', False otherwise.
+ """
+
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ for obj_a in a:
+ if not all(contains(scene, obj_a, obj_b) for obj_b in b):
+ return False
+
+ return True
+
+
+def contains_any(scene: trimesh.Scene, a: Union[str, list[str]], b: Union[str, list[str]]) -> bool:
+ """
+ Check if any object in list 'a' contains any object in list 'b' within the given scene.
+
+ Parameters:
+ - scene: The trimesh.Scene instance.
+ - a: Name or list of names of objects to check for containment.
+ - b: Name or list of names of objects that might be contained.
+
+ Returns:
+ - True if any object in list 'a' contains any object in list 'b', False otherwise.
+ """
+
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ for obj_a in a:
+ if any(contains(scene, obj_a, obj_b) for obj_b in b):
+ return True
+
+ return False
+
+
+def has_line_of_sight(scene: trimesh.Scene, a: Union[str, list[str]], b: Union[str, list[str]], num_samples: int = 100) -> bool:
+ """
+ Check if any object in list 'a' in the scene has a line of sight to any object in list 'b'.
+
+ Parameters:
+ - scene: The trimesh.Scene instance.
+ - a: Name or list of names of objects from which line of sight is checked.
+ - b: Name or list of names of objects to which line of sight is checked.
+ - num_samples: Number of points to sample from each object for ray casting.
+
+ Returns:
+ - True if any object in list 'a' has a line of sight to any object in list 'b', False otherwise.
+ """
+
+ # Ensure 'a' and 'b' are lists
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ a = iu.meshes_from_names(scene, a)
+ b = iu.meshes_from_names(scene, b)
+
+ # Check line of sight for each object in 'a' against any object in 'b'
+ for obj_a in a:
+ # Sample points from the surface of object 'a'
+ points_a = obj_a.sample(num_samples)
+
+ combined_mesh = trimesh.util.concatenate([mesh for name, mesh in scene.geometry.items() if mesh != obj_a])
+
+ for obj_b in b:
+ # Sample points from the surface of object 'b'
+ points_b = obj_b.sample(num_samples)
+
+ # Create rays from points on 'a' to points on 'b'
+ ray_origins = np.tile(points_a, (num_samples, 1))
+ ray_directions = np.repeat(points_b, num_samples, axis=0) - ray_origins
+ ray_directions /= np.linalg.norm(ray_directions, axis=1)[:, None]
+
+ # Check for intersections with the combined mesh
+ locations, index_ray, index_tri = combined_mesh.ray_pyembree.intersects_location(ray_origins, ray_directions, multiple_hits=False)
+
+ # Check if point is reached
+ for i in range(index_ray.shape[0]):
+ index = index_ray[i]
+ hit_location = locations[i]
+
+ # Check if any intersection is close to the point
+ if np.linalg.norm(points_b[index // num_samples] - hit_location) < 1e-6:
+ return True
+
+ return False
+
+def freespace_2d(scene: trimesh.Scene, a: Union[str, list[str]], b: Union[str, list[str]]) -> float:
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ a_meshes = iu.meshes_from_names(scene, a)
+
+ b_meshes = iu.meshes_from_names(scene, b)
+
+ total_projected_area = sum(iu.project_to_xy_path2d(mesh).area for mesh in b_meshes)
+
+
+ available_area = sum(iu.project_to_xy_path2d(mesh).area for mesh in a_meshes)
+
+
+ percent_available = ((available_area - total_projected_area) / available_area) * 100
+
+ return percent_available
+
+def rasterize_space_with_obstacles(scene, a: Union[str, list[str]], b: Union[str, list[str]], start_location, end_location, cell_size=1.0, visualize=False):
+ """
+ Rasterize the union of multiple space polygons while considering obstacle polygons,
+ then find and visualize the shortest path from start to end.
+
+ Parameters:
+ - space_polygons: list of shapely.geometry.polygon.Polygon objects representing the main spaces
+ - obstacle_polygons: list of shapely.geometry.polygon.Polygon objects representing obstacles
+ - start_location: tuple (x, y) representing the start location
+ - end_location: tuple (x, y) representing the end location
+ - cell_size: size of each cell in the grid
+ - visualize: boolean, if True, visualize the union of spaces, obstacles, and the shortest path
+
+ Returns:
+ - graph: A networkx.Graph object representing the rasterized union of spaces minus the obstacles
+ - path: list of nodes representing the shortest path from start to end
+ """
+ def is_close_to_any_node(neighbor, graph, threshold=1e-6):
+ for node in graph.nodes():
+ distance = np.linalg.norm(np.array(neighbor) - np.array(node))
+ if distance < threshold:
+ return node
+ return None
+
+
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ a_meshes = iu.meshes_from_names(scene, a)
+ b_meshes = iu.meshes_from_names(scene, b)
+
+ space_polygons = [iu.project_to_xy_poly(mesh) for mesh in a_meshes]
+ obstacle_polygons = [iu.project_to_xy_poly(mesh) for mesh in b_meshes]
+
+ # Get the union of all space polygons
+ union_space = unary_union(space_polygons)
+
+ # Get bounding box of the union space
+ minx, miny, maxx, maxy = union_space.bounds
+
+ # Create a grid over the bounding box
+ x_coords = np.arange(minx, maxx, cell_size)
+ y_coords = np.arange(miny, maxy, cell_size)
+
+ graph = nx.Graph()
+
+ # For visualization
+ if visualize:
+ fig, ax = plt.subplots()
+ for space in space_polygons:
+ if isinstance(space, Polygon):
+ x, y = space.exterior.xy
+ ax.fill(x, y, alpha=0.5) # Fill the space
+ ax.plot(x, y, color='black') # Plot the space boundary
+ elif isinstance(space, MultiPolygon):
+ for sub_space in space.geoms:
+ x, y = sub_space.exterior.xy
+ ax.fill(x, y, alpha=0.5)
+ ax.plot(x, y, color='black')
+
+ for obstacle in obstacle_polygons:
+ if isinstance(obstacle, Polygon):
+ x, y = obstacle.exterior.xy
+ ax.fill(x, y, color='grey') # Fill the obstacles
+ ax.plot(x, y, color='black') # Plot the obstacle boundary
+ elif isinstance(obstacle, MultiPolygon):
+ for sub_obstacle in obstacle.geoms:
+ x, y = sub_obstacle.exterior.xy
+ ax.fill(x, y, color='grey')
+ ax.plot(x, y, color='black')
+
+ # For each cell in the grid, check if its center is inside the union space and outside all obstacle polygons
+ for x in x_coords:
+ for y in y_coords:
+ cell_center = Point(x + cell_size / 2, y + cell_size / 2)
+ if cell_center.within(union_space) and all(not cell_center.within(obstacle) for obstacle in obstacle_polygons):
+ graph.add_node((x + cell_size / 2, y + cell_size / 2))
+
+ # For visualization
+ if visualize:
+ ax.plot(cell_center.x, cell_center.y, 'bo', markersize=3) # Plot the point inside the union space and outside obstacles
+
+ # Connect each node to its neighboring nodes
+ for node in graph.nodes():
+ x, y = node
+ neighbors = [
+ (x + cell_size, y),
+ (x - cell_size, y),
+ (x, y + cell_size),
+ (x, y - cell_size)
+ ]
+ for neighbor in neighbors:
+ closest_node = is_close_to_any_node(neighbor, graph)
+ if closest_node is not None:
+ graph.add_edge(node, closest_node)
+
+
+ # Find the closest nodes to the start and end locations
+ start_node = min(graph.nodes(), key=lambda node: np.linalg.norm(np.array(node) - np.array(start_location)))
+ end_node = min(graph.nodes(), key=lambda node: np.linalg.norm(np.array(node) - np.array(end_location)))
+
+
+ # Calculate the shortest path using Dijkstra's algorithm
+ path = nx.shortest_path(graph, source=start_node, target=end_node, weight='weight')
+
+ # Visualize the path
+ if visualize:
+ path_x = [x for x, y in path]
+ path_y = [y for x, y in path]
+ ax.plot(path_x, path_y, c='red', linewidth=2, label='Shortest Path')
+ ax.scatter([start_node[0], end_node[0]], [start_node[1], end_node[1]], c='green', s=100, label='Start & End')
+ plt.legend()
+ plt.title('Shortest Path from Start to End')
+ plt.show()
+
+ return graph, path
+
+def angle_alignment_cost_tagged(state: state_def.State, a: Union[str, list[str]], b: Union[str, list[str]], b_tags=None, visualize=False):
+ """
+ Return the dot product between the axes of a and the normal of the closest edge of b
+ """
+ if isinstance(a, str):
+ a = [a]
+
+ b_objs = iu.blender_objs_from_names(b)
+ b_surfs = []
+ for b_obj in b_objs:
+ b_surf = tagging.extract_tagged_faces(b_obj, b_tags)
+ b_surfs.append(b_surf)
+
+ b_surf_names = []
+ for i, b_surf in enumerate(b_surfs):
+ add_to_scene(state.trimesh_scene, b_surf)
+ b_surf_names.append(b_surf.name)
+
+ res = angle_alignment_cost_base(state, a, b_surf_names, visualize)
+
+ for b_surf_name in b_surf_names:
+ iu.delete_obj(state.trimesh_scene, b_surf_name)
+
+ return res
+
+def angle_alignment_cost_base(state: state_def.State, a: Union[str, list[str]], b: Union[str, list[str]], visualize=False):
+ """
+ Return the dot product between the axes of a and the normal of the closest edge of b
+ """
+ # print(f'{a=}, {b=}')
+ scene = state.trimesh_scene
+ a_meshes = iu.meshes_from_names(scene, a)
+ b_meshes = iu.meshes_from_names(scene, b)
+ b_edges = []
+ for b_name, b_mesh in zip(b, b_meshes):
+ b_poly = iu.project_to_xy_poly(b_mesh)
+ if b_poly is not None:
+ if isinstance(b_poly, Polygon):
+ for i, coord in enumerate(b_poly.exterior.coords[:-1]):
+ start, end = coord, b_poly.exterior.coords[i + 1]
+ if np.isclose(start, end).all():
+ continue
+ b_edges.append((LineString([start, end]), b_name))
+ elif isinstance(b_poly, MultiPolygon):
+ for sub_poly in b_poly.geoms:
+ for i, coord in enumerate(sub_poly.exterior.coords[:-1]):
+ start, end = coord, sub_poly.exterior.coords[i + 1]
+ if np.isclose(start, end).all():
+ continue
+ b_edges.append((LineString([start, end]), b_name))
+ else:
+ for edge3d in b_mesh.edges:
+ start = b_mesh.vertices[edge3d[0]][:2]
+ end = b_mesh.vertices[edge3d[1]][:2]
+ if np.isclose(start, end).all():
+ continue
+ b_edges.append((LineString([start, end]), b_name))
+
+ a_blender_objs = iu.blender_objs_from_names(a)
+
+ if visualize:
+ fig, ax = plt.subplots()
+ for edge, _ in b_edges:
+ x, y = edge.xy
+ ax.plot(x, y, color='red', linewidth=1, label='B Edges')
+
+ score = 0
+
+ for a_name, a_obj, a_mesh in zip(a, a_blender_objs, a_meshes):
+ _, axis = get_axis(state, a_obj)
+ axis = axis[:2]
+ a_poly = iu.project_to_xy_poly(a_mesh)
+
+ if a_poly is not None:
+ if isinstance(a_poly, Polygon):
+ a_centroid = a_poly.centroid
+ elif isinstance(a_poly, MultiPolygon):
+ a_centroid = a_poly.centroid
+ else:
+ a_centroid = Point(a_mesh.vertices[:, :2].mean(axis=0))
+
+ filtered_b_edges = [edge for edge, b_name in b_edges if b_name != a_name]
+ if len(filtered_b_edges) == 0:
+ continue
+ closest_line = iu.closest_edge_to_point_edge_list(filtered_b_edges, a_centroid)
+
+ dx = closest_line.xy[0][1] - closest_line.xy[0][0] # x1 - x0
+ dy = closest_line.xy[1][1] - closest_line.xy[1][0] # y1 - y0
+
+ # Candidate normal vectors (perpendicular to edge)
+ normal_vector_1 = np.array([dy, -dx])
+ normal_vector_2 = -normal_vector_1
+
+ # Normalize the vectors
+ normal_vector_1 /= np.linalg.norm(normal_vector_1)
+ normal_vector_2 /= np.linalg.norm(normal_vector_2)
+
+ dot1 = np.dot(axis, normal_vector_1)
+ dot2 = np.dot(axis, normal_vector_2)
+
+ score1 = -dot1 / 2 + 0.5
+ score2 = -dot2 / 2 + 0.5
+
+ score += min(score1, score2)
+
+ if visualize:
+ if a_poly is not None:
+ if isinstance(a_poly, Polygon):
+ x, y = a_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+ elif isinstance(a_poly, MultiPolygon):
+ for sub_poly in a_poly.geoms:
+ x, y = sub_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+ else:
+ x, y = a_mesh.vertices[:, 0], a_mesh.vertices[:, 1]
+ ax.scatter(x, y, color='blue', label='Vertices a')
+
+ ax.arrow(a_centroid.x, a_centroid.y, axis[0], axis[1], head_width=0.15, head_length=0.25, fc='green', ec='green', label='Axis of a')
+ x, y = closest_line.xy
+ ax.plot(x, y, color="green", linewidth=2.5, label='Closest Edge')
+ ax.plot(a_centroid.x, a_centroid.y, 'o', color='black', label='Centroid of a')
+ mid_point = closest_line.interpolate(0.5, normalized=True)
+ ax.arrow(mid_point.x, mid_point.y, normal_vector_1[0], normal_vector_1[1], head_width=0.15, head_length=0.25, fc='yellow', ec='yellow', label='Normal Vector 1')
+ ax.arrow(mid_point.x, mid_point.y, normal_vector_2[0], normal_vector_2[1], head_width=0.15, head_length=0.25, fc='orange', ec='orange', label='Normal Vector 2')
+
+ if visualize:
+ ax.set_title('Polygons, Closest Edge and Normal')
+ ax.set_aspect('equal')
+ ax.grid(True)
+ plt.show()
+
+ return score
+
+def angle_alignment_cost(
+ state: state_def.State,
+ a: Union[str, list[str]],
+ b: Union[str, list[str]],
+ b_tags=None,
+ visualize=False
+):
+ if b_tags is not None:
+ return angle_alignment_cost_tagged(state, a, b, b_tags, visualize)
+ return angle_alignment_cost_base(state, a, b, visualize)
+
+def focus_score(state: state_def.State, a: Union[str, list[str]], b: str, visualize = False):
+ """
+ The how much objects in a focus on b
+ """
+ scene = state.trimesh_scene
+ if isinstance(a, str):
+ a = [a]
+
+
+ a_meshes = iu.meshes_from_names(scene, a)
+ a_blender_objs = iu.blender_objs_from_names(a)
+ b_mesh = iu.meshes_from_names(scene, b)[0]
+
+ a_polys = [iu.project_to_xy_poly(mesh) for mesh in a_meshes]
+ b_poly = iu.project_to_xy_poly(b_mesh)
+
+ if visualize:
+ # Plotting the polygons and normals
+ fig, ax = plt.subplots()
+ if isinstance(b_poly, Polygon):
+ x, y = b_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+ elif isinstance(b_poly, MultiPolygon):
+ for sub_poly in b_poly.geoms:
+ x, y = sub_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+
+ score = 0
+ for a_poly, a_mesh, a_obj in zip(a_polys, a_meshes, a_blender_objs):
+ axis = get_axis(state, a_obj)[1][:2]
+ a_centroid = a_poly.centroid
+ b_centroid = b_poly.centroid
+
+ # turn centroids to np array
+ a_centroid = np.array([a_centroid.x, a_centroid.y])
+ b_centroid = np.array([b_centroid.x, b_centroid.y])
+
+ focus_vec = b_centroid - a_centroid
+ focus_vec /= np.linalg.norm(focus_vec)
+
+ if visualize:
+ # Plotting the polygons
+ if isinstance(a_poly, Polygon):
+ x, y = a_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+ elif isinstance(a_poly, MultiPolygon):
+ for sub_poly in a_poly.geoms:
+ x, y = sub_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+
+ # plot axis
+ ax.arrow(a_centroid[0], a_centroid[1], axis[0], axis[1], head_width=0.15, head_length=0.25, fc='green', ec='green', label='Axis of a')
+
+ # Highlight centroid of a
+ ax.plot(a_centroid[0], a_centroid[1], 'o', color='black', label='Centroid of a')
+
+ # Plot the outward normal vector
+ ax.arrow(a_centroid[0], a_centroid[1], focus_vec[0], focus_vec[1], head_width=0.15, head_length=0.25, fc='yellow', ec='yellow', label='Focus vector')
+
+ score += -np.dot(axis, focus_vec)/2 + 0.5
+
+ if visualize:
+ # Set axis properties
+ ax.set_title('Polygons, Focus Vector')
+ ax.set_aspect('equal')
+ ax.grid(True)
+ # ax.legend(loc="upper left")
+ plt.show()
+
+ return score #/ len(a)
+
+def edge(scene, surface_name: str):
+ surface = iu.meshes_from_names(scene, surface_name)[0]
+ outline_3d = surface.outline()
+ return outline_3d
+
+def min_dist_2d(scene, a: Union[str, list[str]], b, visualize=False):
+ """
+ projects onto b and finds the min distance between a and b
+ """
+ if isinstance(a, str):
+ a = [a]
+
+ if visualize:
+ fig, ax = plt.subplots()
+ min_dist = np.inf
+
+ b_path2d, to_3D = b.to_planar()
+ plane_normal, plane_origin = iu.get_plane_from_3dmatrix(to_3D)
+
+ a_meshes = iu.meshes_from_names(scene, a)
+
+ a_projections = [trimesh.path.polygons.projected(mesh, plane_normal, plane_origin) for mesh in a_meshes]
+ # Measure the distance
+ for a_proj in a_projections:
+ source_geom = a_proj
+ target_geom = b_path2d.polygons_closed[0].exterior
+ dist = source_geom.distance(target_geom)
+ if dist < min_dist:
+ if visualize:
+ pt_a, pt_b = nearest_points(source_geom, target_geom)
+ ax.plot([pt_a.x, pt_b.x], [pt_a.y, pt_b.y], color='red')
+ #plot source and target geoms
+ iu.plot_geometry(ax, source_geom, 'blue')
+ iu.plot_geometry(ax, target_geom, 'green')
+ min_dist = dist
+
+ if visualize:
+ plt.show()
+ return min_dist
+
+def min_dist_boundary(scene: Scene, a: Union[str, list[str]], boundary):
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(boundary, trimesh.path.path.Path3D):
+ pass
+ elif isinstance(boundary, trimesh.path.path.Path2D):
+ pass
+ else:
+ raise TypeError(f'Unhandled type {boundary=}')
+
+class ConstraintViolated(Exception):
+ pass
+
+FATAL = True
+def constraint_violated(message):
+ if FATAL:
+ raise ConstraintViolated(message)
+ else:
+ print(f'{ConstraintViolated.__name__}: {message}')
+
+def constrain_contact(
+ res: ContactResult,
+ should_touch=True,
+ max_depth=1e-2,
+ #normal_dir=None,
+ #normal_dot_min=None,
+ #normal_dot_max=None
+):
+
+ if res.hit is None:
+ return False # arises from an internal-contact query on a set of one element
+
+ if should_touch is not None and should_touch != res.hit:
+ if should_touch:
+ return False #constraint_violated(f'At least one of {res.names} must touch eachother')
+ else:
+ return False #constraint_violated(f'{res.names} must not touch')
+
+ if res.hit and max_depth is not None:
+ observed_depth = max(c.depth for c in res.contacts)
+ if observed_depth > max_depth:
+ return False #constraint_violated(f'Contact between {res.names} penetrates by depth {observed_depth} > {max_depth}')
+ return True
+
+def constrain_dist(res: dict, min=None, max=None):
+
+ if res.data is None: # results from internal distance check on 1 object
+ print("res data error")
+ return
+
+ if not (
+ min is None or
+ min < res.dist
+ ):
+ return False
+
+ if not (
+ max is None or
+ max > res.dist
+ ):
+ return False
+ return True
+
+def constrain_dist_soft(res: dict, min=None, max=None):
+
+ if res.data is None: # results from internal distance check on 1 object
+ print("res data error")
+ return
+
+ if res.dist < min:
+ return min - res.dist
+
+ if res.dist > max:
+ return res.dist - max
+ return 0
+
+def touching_soft(scene, a, b):
+
+ res = any_touching(scene, a, b)
+
+
+ if res.hit is None:
+ print("res hit error")
+ return np.inf # arises from an internal-contact query on a set of one element
+
+ if res.hit:
+ observed_depth = max(c.depth for c in res.contacts)
+ return observed_depth
+ else:
+ res = min_dist(scene, a, b)
+ if res.data is None:
+ return np.inf
+ else:
+ return res.dist
+
+def dist_soft_score(res: dict, min, max):
+ if res.data is None: # results from internal distance check on 1 object
+ return 0
+
+ if res.dist > max:
+ return res.dist - max
+ elif res.dist < min:
+ return min - res.dist
+ else:
+ return 0
+
+_accessibility_vis_seen_objs = set() # used to make vis=True below less spammy
+
+def accessibility_cost_cuboid_penetration(
+ scene: trimesh.Scene,
+ a: Union[str, list[str]],
+ b: Union[str, list[str]],
+ normal_dir: np.ndarray,
+ dist: float,
+ bvh_cache: dict = None,
+ vis=False
+):
+
+ """
+ Extrude the bbox of a by dist in the direction of normal_dir, and check for collisions with b
+ Return the maximum distance that any part of b penetrates this extrusion
+ """
+
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ if len(a) == 0 or len(b) == 0:
+ return 0
+
+ a_free_col = trimesh.collision.CollisionManager()
+
+ # find which of +X, -X +Y, -Y, +Z, -Z is the normal_dir. Only these values are supported
+ if (
+ not np.isclose(np.linalg.norm(normal_dir), 1) or
+ np.isclose(normal_dir, 0).sum() != 2
+ ):
+ raise ValueError(f'Invalid normal_dir {normal_dir=}, expected +X, -X, +Y, -Y, +Z, -Z')
+ normal_axis = np.argmax(np.abs(normal_dir))
+ normal_sign = np.sign(normal_dir[normal_axis])
+
+ visobjs = []
+ for name in a:
+
+ T, g = scene.graph[name]
+ geom = scene.geometry[g]
+
+ # create an extrusion of the bbox by dist in the direction of normal_dir
+ bpy_obj = bpy.data.objects[name]
+
+ freespace_exts = np.copy(np.array(bpy_obj.dimensions))
+ freespace_exts[normal_axis] = dist
+ freespace_box = trimesh.creation.box(freespace_exts)
+
+ bbox = np.array(bpy_obj.bound_box)
+ origin_to_bbox_center = bbox.mean(axis=0)
+ extent_from_real_origin = bbox[0 if normal_sign < 0 else -1][normal_axis]
+
+ offset_vec = normal_dir * (dist/2 + extent_from_real_origin - origin_to_bbox_center[normal_axis])
+ total_offset_vec = origin_to_bbox_center + offset_vec
+
+ freespace_box_transform = np.array(bpy_obj.matrix_world) @ trimesh.transformations.translation_matrix(total_offset_vec)
+
+ a_free_col.add_object(name, freespace_box, freespace_box_transform)
+
+ visobjs.append(geom.apply_transform(T))
+
+ visobjs.append(freespace_box.apply_transform(freespace_box_transform))
+
+ b_col = iu.col_from_subset(scene, b, bvh_cache=bvh_cache)
+ hit, contacts = b_col.in_collision_other(a_free_col, return_data=True)
+
+ if vis:
+ bobjs = iu.meshes_from_names(scene, b)
+ print(f"{np.round(origin_to_bbox_center, 3)=} {extent_from_real_origin} {bpy_obj.dimensions}")
+ if not all(name in _accessibility_vis_seen_objs for name in a + b):
+ trimesh.Scene(visobjs + bobjs).show()
+ _accessibility_vis_seen_objs.update(a + b)
+
+ if hit:
+ return max(c.depth for c in contacts)
+ else:
+ return 0
+
+@gin.configurable
+def accessibility_cost(scene, a, b, normal, visualize=False, fast = True):
+ """
+ Computes how much objs b block front access to a. b obj blockages are not summed.
+ the closest b obj to a is taken as the representative blockage
+ """
+
+ if isinstance(a, str):
+ a = [a]
+ if isinstance(b, str):
+ b = [b]
+
+ b = [b_name for b_name in b if b_name not in a]
+ if len(b) == 0:
+ return 0
+
+ if visualize:
+ fig, ax = plt.subplots()
+ a_trimeshes = iu.meshes_from_names(scene, a)
+ b_trimeshes = iu.meshes_from_names(scene, b)
+
+ a_objs = iu.blender_objs_from_names(a)
+ b_objs = iu.blender_objs_from_names(b)
+
+ score = 0
+ for a_name, a_obj, a_trimesh in zip(a, a_objs, a_trimeshes):
+
+ a_centroid = a_trimesh.centroid
+
+ front_plane_pt = a_centroid
+ front_plane_normal = np.array(a_obj.matrix_world.to_3x3() @ Vector(normal))
+
+ a_centroid_proj = a_centroid - np.dot(a_centroid - front_plane_pt, front_plane_normal) * front_plane_normal
+
+
+ if fast:
+ # get the closest centroid in b and the mesh that it belongs to
+ b_centroids = [b_trimesh.centroid for b_trimesh in b_trimeshes]
+ distances = [np.linalg.norm(pt - a_centroid_proj) for pt in b_centroids]
+ min_index = np.argmin(distances)
+ b_closest_pt = b_centroids[min_index]
+ b_chosen = b[min_index]
+ else:
+ # might need to change this to closest pt on the frontal plane
+ res = min_dist(scene, a_name, b)
+ b_chosen = res.names[1] if res.names[0] == a_name else res.names[0]
+ b_closest_pt = res.data.point(b_chosen)
+
+ centroid_to_b = b_closest_pt - a_centroid_proj
+
+ dist = np.linalg.norm(centroid_to_b)
+ bounds = iu.meshes_from_names(scene, b_chosen)[0].bounds
+ diag_length = np.linalg.norm(bounds[1] - bounds[0])
+ if np.dot(centroid_to_b, front_plane_normal) < 0:
+ continue
+ # cos theta/dist
+ score += (np.dot(centroid_to_b, front_plane_normal) / dist**2) * diag_length
+ if visualize:
+ ax.plot([a_centroid_proj[0], b_closest_pt[0]], [a_centroid_proj[1], b_closest_pt[1]], color='red')
+ #plot source and target geoms
+ iu.plot_geometry(ax, a_trimesh, 'blue')
+ iu.plot_geometry(ax, iu.meshes_from_names(scene, b_chosen)[0], 'green')
+ # plot front plane
+ # plot_geometry(ax, planes.extract_tagged_plane(a_obj, a_tag, a_front_plane), 'black')
+ # plot centroid
+ ax.plot(a_centroid_proj[0], a_centroid_proj[1], 'o', color='black', label='Centroid of a')
+
+ if visualize:
+ plt.show()
+ return score
+
+
+def center_stable_surface(scene, a, state):
+ """
+ center a objects on their assigned surfaces.
+ """
+ if isinstance(a, str):
+ a = [a]
+
+ score = 0
+ a_trimeshes = iu.meshes_from_names(
+ scene,
+ [state.objs[ai].obj.name for ai in a]
+ )
+
+ for name, mesh in zip(a, a_trimeshes):
+ obj_state = state.objs[name]
+ obj = obj_state.obj
+ for i, relation_state in enumerate(obj_state.relations):
+ relation = relation_state.relation
+ parent_obj = state.objs[relation_state.target_name].obj
+ obj_tags = relation.child_tags
+ parent_tags = relation.parent_tags
+ parent_all_planes = state.planes.get_tagged_planes(parent_obj, parent_tags)
+ obj_all_planes = state.planes.get_tagged_planes(obj, obj_tags)
+ parent_plane = parent_all_planes[relation_state.parent_plane_idx]
+ obj_plane = obj_all_planes[relation_state.child_plane_idx]
+
+ if relation_state.parent_plane_idx >= len(parent_all_planes):
+ logging.warning(f'{parent_obj.name=} had too few planes ({len(parent_all_planes)}) for {relation_state}')
+ return False
+ if relation_state.child_plane_idx >= len(obj_all_planes):
+ logging.warning(f'{obj.name=} had too few planes ({len(obj_all_planes)}) for {relation_state}')
+ return False
+
+ splitted_parent = state.planes.extract_tagged_plane(parent_obj, parent_tags, parent_plane)
+ parent_trimesh = add_to_scene(state.trimesh_scene, splitted_parent, preprocess=True)
+ # splitted_obj = planes.extract_tagged_plane(obj, obj_tags, obj_plane)
+ # add_to_scene(state.trimesh_scene, splitted_obj, preprocess=True)
+ obj_centroid = mesh.centroid
+ parent_centroid = parent_trimesh.centroid
+ score += np.linalg.norm(obj_centroid - parent_centroid)
+
+ iu.delete_obj(scene, splitted_parent.name)
+
+ return score
+
+
+def reflectional_asymmetry_score(scene, a: Union[str, list[str]], b: str, use_long_plane=True):
+ """
+ Computes the reflectional asymmetry score between a and b
+ """
+ if isinstance(a, str):
+ a = [a]
+ if b is None or len(b) == 0:
+ return 0
+
+ a_trimeshes = iu.meshes_from_names(scene, a)
+ b_trimesh = iu.meshes_from_names(scene, b)[0]
+
+ a_objs = iu.blender_objs_from_names(a)
+ b_obj = iu.blender_objs_from_names(b)[0]
+
+ bbox = b_trimesh.bounding_box_oriented
+ vertices = bbox.vertices
+
+ mid_planes = get_cardinal_planes_bbox(vertices)
+ if use_long_plane:
+ plane_pt, plane_normal = mid_planes[0]
+ else:
+ plane_pt, plane_normal = mid_planes[1]
+
+
+
+ return symmetry.calculate_reflectional_asymmetry(a_objs, plane_pt, plane_normal)
+
+
+def coplanarity_cost_pair(scene, a: str, b: str):
+ """
+ Computes the coplanarity cost between a and b
+ """
+ a_trimesh = iu.meshes_from_names(scene, a)[0]
+ b_trimesh = iu.meshes_from_names(scene, b)[0]
+
+ a_obj = iu.blender_objs_from_names(a)[0]
+ b_obj = iu.blender_objs_from_names(b)[0]
+
+ a_trimesh_bbox = a_trimesh.bounding_box_oriented
+ b_trimesh_bbox = b_trimesh.bounding_box_oriented
+
+ object1_planes = []
+ object2_planes = []
+
+ # Helper function to check if a normal is close to any in the list
+ def is_normal_new(normal, normals_list):
+ normals_np = np.array(normals_list)
+ if len(normals_list) > 0:
+ return not np.any(np.all(np.isclose(normals_np, normal, atol=1e-3), axis=1))
+ return True
+
+ for i in range(len(a_trimesh_bbox.faces)):
+ normal = a_trimesh_bbox.face_normals[i]
+ if is_normal_new(normal, [n for _, n in object1_planes]):
+ object1_planes.append((a_trimesh_bbox.vertices[a_trimesh_bbox.faces[i]][0], normal))
+
+ for i in range(len(b_trimesh_bbox.faces)):
+ normal = b_trimesh_bbox.face_normals[i]
+ if is_normal_new(normal, [n for _, n in object2_planes]):
+ object2_planes.append((b_trimesh_bbox.vertices[b_trimesh_bbox.faces[i]][0], normal))
+
+ # Calculate angle cost matrix for bipartite matching
+ angle_cost_matrix = np.zeros((len(object1_planes), len(object2_planes)))
+ for j, plane1 in enumerate(object1_planes):
+ for k, plane2 in enumerate(object2_planes):
+ angle_cost = 1 - np.dot(plane1[1], plane2[1])
+ angle_cost_matrix[j, k] = angle_cost
+
+ # Perform linear sum assignment based on angle alignment
+ row_ind, col_ind = linear_sum_assignment(angle_cost_matrix)
+
+ # Calculate total costs (angle + distance) for the optimal matching
+ total_costs = []
+ for r, c in zip(row_ind, col_ind):
+ distance_cost = iu.distance_to_plane(object1_planes[r][0], object2_planes[c][0], object2_planes[c][1])
+ total_cost = angle_cost_matrix[r, c] + distance_cost # Sum angle and distance costs
+ total_costs.append(total_cost)
+ total_costs = sorted(total_costs)
+
+ return sum(total_costs[:-2])
+
+def coplanarity_cost(scene, a: Union[str, list[str]]):
+ """
+ Computes the coplanarity cost between a and b
+ """
+ if isinstance(a, str):
+ a = [a]
+
+ a_trimeshes = iu.meshes_from_names(scene, a)
+ a_objs = iu.blender_objs_from_names(a)
+
+ # Order objects by principal axis
+ ordered_objects = iu.order_objects_by_principal_axis(a_objs)
+
+ all_total_costs = [] # To store the sum of angle and distance costs for each optimal matching
+
+ # Iterate over pairs of consecutive objects
+ for i in range(len(ordered_objects) - 1):
+ all_total_costs.append(coplanarity_cost_pair(scene, ordered_objects[i].name, ordered_objects[i + 1].name))
+
+ # Calculate the final cost as the sum of the remaining costs
+ final_cost = sum(all_total_costs) / len(a_objs)
+
+ return final_cost
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/__init__.py b/infinigen/core/constraints/example_solver/__init__.py
new file mode 100644
index 000000000..7a3d280f8
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/__init__.py
@@ -0,0 +1,3 @@
+from . import room
+from .state_def import State
+from .solve import Solver
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/annealing.py b/infinigen/core/constraints/example_solver/annealing.py
new file mode 100644
index 000000000..2db976694
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/annealing.py
@@ -0,0 +1,372 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick, Karhan Kayan
+
+from collections import defaultdict
+import logging
+import os
+import time
+import copy
+import typing
+from pprint import pprint
+
+import matplotlib.pyplot as plt
+import bpy
+import numpy as np
+import tqdm
+import pandas as pd
+import gin
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ evaluator
+)
+from .moves import Move
+from .state_def import State
+from infinigen.core.util import blender as butil
+
+from infinigen.core.constraints.constraint_language import util as impl_util
+
+logger = logging.getLogger(__name__)
+
+BPY_GARBAGE_COLLECT_FREQUENCY = 20 # every X optim steps
+
+@gin.configurable
+class SimulatedAnnealingSolver:
+
+ def __init__(
+ self,
+ max_invalid_candidates,
+ initial_temp,
+ final_temp,
+ finetune_pct,
+ checkpoint_best=False,
+ output_folder=None,
+ visualize=False,
+ print_report_freq=10,
+ print_breakdown_freq=0
+ ) -> None:
+
+ self.initial_temp = initial_temp
+ self.final_temp = final_temp
+ self.max_invalid_candidates = max_invalid_candidates
+ self.finetune_pct = finetune_pct
+
+ self.print_report_freq = print_report_freq
+ self.print_breakdown_freq = print_breakdown_freq
+
+ self.checkpoint_best = checkpoint_best
+ if checkpoint_best:
+ raise NotImplementedError(f'{checkpoint_best=}')
+
+ self.output_folder = output_folder
+ self.visualize = visualize
+
+ self.cooling_rate = None
+ self.last_eval_result = None
+
+ self.eval_memo = {}
+
+ def save_stats(self, path):
+
+ if len(self.stats) == 0:
+ return
+
+ df = pd.DataFrame.from_records(self.stats)
+
+ logger.info(f'Saving stats {path}')
+ df.to_csv(path)
+
+ fig, ax1 = plt.subplots()
+ ax1.set_xlabel('Iteration')
+ ax1.set_ylabel('Score', color='C0')
+ ax1.plot(df['curr_iteration'], df['loss'], color='C0')
+
+ #ax2 = ax1.twinx()
+ #ax2.set_ylabel('Move Time', color='C1')
+ #ax2.plot(df['curr_iteration'], df['move_dur'], color='C1')
+
+ figpath = path.parent/(path.stem+'.png')
+ logger.info(f'Saving plot {figpath}')
+ plt.savefig(figpath)
+ plt.close()
+
+ logger.info(f"Total elapsed {path.stem} {self.stats[-1]['elapsed']:.2f}")
+
+ def reset(self, max_iters):
+
+ self.curr_iteration = 0
+ self.stats = []
+ self.curr_result = None
+ self.best_loss = None
+ self.eval_memo = {}
+
+ self.optim_start_time = time.perf_counter()
+ self.max_iterations = max_iters
+
+ if max_iters == 0:
+ self.cooling_rate = 0
+ else:
+ steps = (max_iters * (1 - self.finetune_pct))
+ ratio = self.final_temp / self.initial_temp
+ self.cooling_rate = np.power(ratio, 1/steps)
+
+ logger.debug(f'Reset solver with {max_iters=} cooling_rate={self.cooling_rate:.4f}')
+
+ def checkpoint(self, state):
+
+ filename = os.path.join(self.output_folder, f"checkpoint_state.pkl")
+ state.save(filename)
+
+ if self.visualize:
+ #save score plot
+ plt.plot(self.score_history)
+ plt.savefig(os.path.join(self.output_folder, f"scores.png"))
+ plt.close()
+
+ # render image
+ i = 1
+ while os.path.exists(os.path.join(self.output_folder, f"{i:04}.png")):
+ i += 1
+ bpy.context.scene.render.filepath = os.path.join(self.output_folder, f"{i:04}.png")
+ bpy.ops.render.render(write_still=True)
+
+ def validate_lazy_eval(
+ self,
+ state: State,
+ consgraph: cl.Problem,
+ prop_result: evaluator.EvalResult,
+ filter_domain: r.Domain
+ ):
+
+ test_memo = {}
+ impl_util.DISABLE_BVH_CACHE = True
+ real_result = evaluator.evaluate_problem(consgraph, state, filter_domain, memo=test_memo)
+ impl_util.DISABLE_BVH_CACHE = False
+
+ if real_result.loss() == prop_result.loss():
+ return
+
+ for n in consgraph.traverse(inorder=False):
+ key = evaluator.memo_key(n)
+ if key not in self.eval_memo:
+ continue
+ lazy = self.eval_memo[key]
+ if test_memo[key] == lazy:
+ continue
+
+ print('\n\n INVALID')
+ pprint(n, depth=3)
+ print(f'memo for node is out of sync, got {lazy=} yet {test_memo[key]=}')
+ raise ValueError(f'{real_result.loss()=:.4f} {prop_result.loss()=:.4f}')
+
+ @gin.configurable
+ def evaluate_move(
+ self,
+ consgraph: cl.Node,
+ state: State,
+ move: Move,
+ filter_domain: r.Domain,
+ do_lazy_eval=True,
+ validate_lazy_eval=False
+ ):
+
+ if do_lazy_eval:
+ evaluator.evict_memo_for_move(consgraph, state, self.eval_memo, move)
+ prop_result = evaluator.evaluate_problem(
+ consgraph, state, filter_domain, self.eval_memo
+ )
+ else:
+ prop_result = evaluator.evaluate_problem(
+ consgraph, state, filter_domain, memo={}
+ )
+
+ if validate_lazy_eval:
+ self.validate_lazy_eval(state, consgraph, prop_result, filter_domain)
+
+ return prop_result
+
+ @gin.configurable
+ def retry_attempt_proposals(
+ self,
+ propose_func: typing.Callable,
+ consgraph: cl.Node,
+ state: State,
+ temp: float,
+ filter_domain: r.Domain,
+ ) -> typing.Tuple[Move, evaluator.EvalResult, int]:
+
+ move_gen = propose_func(consgraph, state, filter_domain, temp)
+
+ move = None
+ retry = None
+ for retry, move in enumerate(move_gen):
+
+ if retry == self.max_invalid_candidates:
+ logger.debug(f'{move_gen=} reached {self.max_invalid_candidates=} without succeeding an apply()')
+ break
+
+ succeeded = move.apply(state)
+ if succeeded:
+ evaluator.evict_memo_for_move(consgraph, state, self.eval_memo, move)
+ result = self.evaluate_move(consgraph, state, move, filter_domain)
+ return move, result, retry
+
+ logger.debug(f'{retry=} reverting {move=}')
+ evaluator.evict_memo_for_move(consgraph, state, self.eval_memo, move)
+ move.revert(state)
+
+ else:
+ logger.debug(f'{move_gen=} produced {retry} attempts and none were valid')
+
+ return move, None, retry
+
+ def curr_temp(self) -> float:
+ temp = self.initial_temp * self.cooling_rate ** self.curr_iteration
+ temp = np.clip(temp, self.final_temp, self.initial_temp)
+ return temp
+
+ def metrop_hastings_with_viol(self, prop_result: evaluator.EvalResult, temp: float):
+
+ prop_viol = prop_result.viol_count()
+ curr_viol = self.curr_result.viol_count()
+
+ diff = prop_result.loss() - self.curr_result.loss()
+ log_prob = -diff/temp
+
+ viol_diff = prop_viol - curr_viol
+
+ result = {'diff': diff, 'log_prob': log_prob, 'viol_diff': viol_diff}
+
+ if viol_diff < 0:
+ result['accept'] = True
+ return result
+ elif viol_diff > 0:
+ result['accept'] = False
+ return result
+
+ # standard metropolis-hastings
+ rv = np.log(np.random.uniform())
+ result['accept'] = rv < log_prob
+ return result
+
+ def step(self, consgraph, state, move_gen_func, filter_domain):
+
+ if self.curr_result is None:
+ self.curr_result = evaluator.evaluate_problem(consgraph, state, filter_domain)
+
+ move_start_time = time.perf_counter()
+
+ is_log_step = (
+ self.print_report_freq != 0
+ and self.curr_iteration % self.print_report_freq == 0
+ )
+ is_report_step = (
+ self.print_breakdown_freq != 0
+ and self.curr_iteration % self.print_breakdown_freq == 0
+ )
+
+ temp = self.curr_temp()
+ move, prop_result, retry = self.retry_attempt_proposals(
+ move_gen_func, consgraph, state, temp, filter_domain
+ )
+
+ if prop_result is None:
+ # set null values for logging purposes
+ accept_result = {'accept': None, 'diff': 0, 'log_prob': 0, 'viol_diff': None}
+ else:
+ accept_result = self.metrop_hastings_with_viol(prop_result, temp)
+ if accept_result['accept']:
+ self.curr_result = prop_result
+ move.accept(state)
+ else:
+ evaluator.evict_memo_for_move(consgraph, state, self.eval_memo, move)
+ move.revert(state)
+
+ dt = time.perf_counter() - move_start_time
+ elapsed = time.perf_counter() - self.optim_start_time
+
+ if (
+ (self.print_report_freq != 0 and accept_result['accept'])
+ or is_log_step
+ ):
+
+ n = len(state.objs)
+ move_log = move_gen_func.__name__ if move is None else move
+
+ log_prob = accept_result['log_prob']
+ prob = 1 if log_prob > 7 else np.exp(accept_result['log_prob']) # avoid overflow warnings. clamp to exp = exp(7) ~= 1000
+
+ loss = self.curr_result.loss()
+ viol = self.curr_result.viol_count()
+ diff = accept_result['diff']
+ accept = accept_result['accept']
+ viol_diff = accept_result['viol_diff'] or 0
+
+ logger.info(
+ f"it={self.curr_iteration} {dt=:.3f} {n=} "
+ f"{loss=:.3e} {viol=:.1f} "
+ f"{temp=:.2e} {diff=:.2f} {viol_diff=:.1f} {prob=:.2f} {accept=} "
+ f"{move_log}"
+ )
+
+
+ if is_log_step:
+ self.stats.append(dict(
+ curr_iteration=self.curr_iteration,
+ loss=self.curr_result.loss(),
+ viol=self.curr_result.viol_count(),
+ best_loss=self.best_loss,
+ temp=temp,
+ accept=accept,
+ move_gen=move_gen_func.__name__,
+ move_type=(
+ move.__class__.__name__
+ if move is not None else None
+ ),
+ move_target=(
+ move.name
+ if move is not None and hasattr(move, 'name')
+ else None
+ ),
+ move_dur=dt,
+ elapsed=elapsed,
+ retry=retry
+ ))
+
+ if is_report_step and prop_result is not None:
+ df = prop_result.to_df()
+
+ if self.last_eval_result is not None:
+ last_df = self.last_eval_result.to_df()
+ diff_cols = [
+ c for c in df.columns
+ if (
+ not last_df[c].equals(df[c])
+ or (df[c]["viol_count"] is not None and last_df[c]["viol_count"] > 0)
+ )
+ ]
+ print(self.last_eval_result.viol_count(), self.curr_result.viol_count(), prop_result.viol_count())
+ last_df.index = ['prev_' + x for x in last_df.index]
+ df = pd.concat([last_df[diff_cols], df[diff_cols]])
+
+ print(df)
+
+ if self.curr_iteration % BPY_GARBAGE_COLLECT_FREQUENCY == 0:
+ butil.garbage_collect(butil.get_all_bpy_data_targets())
+
+ if self.curr_iteration != 0 and self.curr_iteration % 50 == 0:
+ print(f'CLUTTER REPORT {self.curr_iteration=}')
+ print(' State Size', len(state.objs))
+ print(' Trimesh', len(state.trimesh_scene.graph.nodes))
+ print(' Objects', len(bpy.data.objects))
+ print(' Meshes', len(bpy.data.meshes))
+ print(' Materials', len(bpy.data.materials))
+ print(' Textures', len(bpy.data.materials))
+
+ self.curr_iteration += 1
+ if prop_result is not None:
+ self.last_eval_result = prop_result
diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py
new file mode 100644
index 000000000..5f60e1e43
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/geometry/dof.py
@@ -0,0 +1,426 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import logging
+
+import gin
+import bpy
+import numpy as np
+from infinigen.core.constraints.example_solver.geometry import stability
+from mathutils import Vector
+import trimesh
+from shapely.geometry import Point, Polygon, MultiPolygon
+import matplotlib.pyplot as plt
+
+from infinigen.core import tags as t, tagging
+from infinigen.core.constraints import constraint_language as cl
+
+from infinigen.core.constraints.example_solver import (
+ state_def
+)
+
+import infinigen.core.util.blender as butil
+import infinigen.core.constraints.example_solver.geometry.validity as validity
+from infinigen.core.constraints.constraint_language.util import meshes_from_names ,delete_obj
+from infinigen.core.constraints.constraint_language import util as iu
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS
+
+
+
+logger = logging.getLogger(__name__)
+
+def stable_against_matrix(point, normal):
+ """
+ Given a point and normal defining a plane, return a 3x3 matrix that
+ restricts movement perpendicular to the plane.
+ """
+ # Normalize the normal vector
+ normal = np.array(normal)
+ normalized_normal = normal / np.linalg.norm(normal)
+
+ # Create a matrix that restricts movement along the normal direction
+ restriction_matrix = np.identity(3) - np.outer(normalized_normal, normalized_normal)
+ return restriction_matrix
+
+def combined_stability_matrix(parent_planes):
+ """
+ Given a list of relations (each a tuple of point and normal),
+ compute the combined 3x3 matrix M.
+ """
+
+ M = np.identity(3)
+ for name, poly in parent_planes:
+ obj = bpy.data.objects[name]
+ poly = obj.data.polygons[poly]
+ point = obj.data.vertices[poly.vertices[0]]
+ M = np.dot(M, stable_against_matrix(point, poly.normal))
+ return M
+
+
+def rotation_constraint(normal):
+ """
+ Given a normal defining a plane, return the axis of rotation allowed by this constraint.
+ """
+ # Normalize the normal vector
+ normal = np.array(normal)
+ normalized_normal = normal / np.linalg.norm(normal)
+
+ return normalized_normal
+
+def combine_rotation_constraints(parent_planes, eps=0.01):
+ """
+ Given a list of normals, compute the combined axis of rotation.
+ If there are conflicting constraints, return None.
+ """
+
+ normals = [
+ bpy.data.objects[name].data.polygons[poly].normal
+ for name, poly in parent_planes
+ ]
+
+ # Start with the first constraint
+ combined_axis = rotation_constraint(normals[0])
+
+ for normal in normals[1:]:
+ axis = rotation_constraint(normal)
+
+ # If the axes are not parallel, there's a conflict
+ if not np.isclose(combined_axis.dot(axis), 1, atol=eps):
+ return None
+
+ # Otherwise, keep the current axis (since they're parallel)
+
+ return combined_axis
+
+
+def rotate_object_around_axis(obj, axis, std, angle=None):
+ """
+ Rotate an object around a given axis.
+ """
+ # Normalize the axis
+ axis = np.array(axis)
+ normalized_axis = axis / np.linalg.norm(axis)
+
+ # If no angle is provided, generate a random angle between 0 and 2*pi
+ if angle is None:
+ angle = np.random.normal(0,std)
+
+ obj.rotation_mode = 'AXIS_ANGLE'
+ obj.rotation_axis_angle = Vector([angle]+ list(normalized_axis))
+
+def check_init_valid(
+ state: state_def.State,
+ name: str,
+ obj_planes: list,
+ assigned_planes: list,
+ margins
+):
+
+ if len(obj_planes) == 0:
+ raise ValueError(f"{check_init_valid.__name__} for {name=} got {obj_planes=}")
+ if len(obj_planes) > 3:
+ raise ValueError(f'{check_init_valid.__name__} for {name=} got {len(obj_planes)=}')
+
+ def get_rot(ind):
+ try:
+ a = obj_planes[ind][0]
+ b = assigned_planes[ind][0]
+ except IndexError:
+ raise ValueError(f'Invalid {ind=} {obj_planes=} {assigned_planes=}')
+
+ a_plane = obj_planes[ind]
+ b_plane = assigned_planes[ind]
+ a_obj = bpy.data.objects[a]
+ b_obj = bpy.data.objects[b]
+
+ a_poly_index = a_plane[1]
+ a_poly = a_obj.data.polygons[a_poly_index]
+ b_poly_index = b_plane[1]
+ b_poly = b_obj.data.polygons[b_poly_index]
+ plane_normal_a = iu.global_polygon_normal(a_obj, a_poly)
+ plane_normal_b = iu.global_polygon_normal(b_obj, b_poly)
+ plane_normal_b = -plane_normal_b
+ rotation_axis = np.cross(plane_normal_a, plane_normal_b)
+
+ if not np.isclose(np.linalg.norm(rotation_axis),0, atol = 1e-03):
+ rotation_axis = rotation_axis / np.linalg.norm(rotation_axis)
+ else:
+ rotation_axis = np.array([0,0,1])
+ dot = plane_normal_a.dot(plane_normal_b)
+ rotation_angle = np.arccos(np.clip(dot, -1, 1))
+ if np.isnan(rotation_angle):
+ raise ValueError(f'Invalid {rotation_angle=}')
+ return a,b,rotation_axis, rotation_angle, plane_normal_b
+
+ def is_rotation_allowed(rotation_axis, reference_normal):
+ # Check if rotation axis is the same as the reference normal (with some tolerance)
+ res = (
+ np.allclose(rotation_axis, reference_normal, atol=1e-02)
+ or np.allclose(rotation_axis, -reference_normal, atol=1e-02)
+ )
+ if not res:
+ dot = rotation_axis.dot(reference_normal)
+ logger.debug(f'{is_rotation_allowed.__name__} got {res=} with {rotation_axis=} {reference_normal=} {dot=}')
+ return res
+
+
+ a, b, rotation_axis, rotation_angle, plane_normal_b = get_rot(0)
+ iu.rotate(state.trimesh_scene, a, rotation_axis, rotation_angle)
+ first_plane_normal = plane_normal_b # Save the normal of the first plane
+
+ dof_remaining = True # Degree of freedom remaining after the first alignment
+
+ # Check and apply rotations for subsequent planes
+ for i in range(1, len(obj_planes)):
+
+ a, b, rotation_axis, rotation_angle, plane_normal_b = get_rot(i)
+
+ if np.isclose(np.linalg.norm(rotation_angle), 0, atol=1e-01):
+ logger.debug(f"no rotation needed for {i=} of {len(obj_planes)}")
+ continue
+
+ rot_allowed = is_rotation_allowed(rotation_axis, first_plane_normal)
+ if dof_remaining and rot_allowed:
+ # Rotate around the normal of the first plane
+ iu.rotate(state.trimesh_scene, a, rotation_axis, rotation_angle)
+ dof_remaining = False # No more degrees of freedom remaining
+ logger.debug(f"rotated {a=} to satisfy assignment {i=}")
+ else:
+ logger.debug(f"dofs failed for {i=} of {len(obj_planes)=}, {rot_allowed=} {dof_remaining=}")
+ return False, None, None
+
+ # Construct the system of linear equations for translation
+ A = []
+ c = []
+ for i in range(len(obj_planes)):
+ a_obj_name, a_poly_index = obj_planes[i]
+ b_obj_name, b_poly_index = assigned_planes[i]
+ margin = margins[i]
+
+ a_obj = bpy.data.objects[a_obj_name]
+ b_obj = bpy.data.objects[b_obj_name]
+
+ a_poly = a_obj.data.polygons[a_poly_index]
+ b_poly = b_obj.data.polygons[b_poly_index]
+
+ # Get global coordinates and normals
+ plane_point_a = iu.global_vertex_coordinates(a_obj, a_obj.data.vertices[a_poly.vertices[0]])
+ plane_point_b = iu.global_vertex_coordinates(b_obj, b_obj.data.vertices[b_poly.vertices[0]])
+ plane_normal_b = iu.global_polygon_normal(b_obj, b_poly)
+ plane_point_b += plane_normal_b * margin
+
+ # Append to the matrix A and vector b for Ax = c
+ A.append(plane_normal_b)
+ c.append(plane_normal_b.dot(plane_point_b - plane_point_a))
+
+ # Solve the linear system
+ A = np.array(A)
+ c = np.array(c)
+
+ t, residuals, rank, s = np.linalg.lstsq(A, c, rcond=None)
+ a_obj_name, a_poly_index = obj_planes[0]
+
+ a_obj = bpy.data.objects[a_obj_name]
+
+ # Check if the solution is valid
+ # You can define a threshold to determine if the residuals are acceptable
+ # Manually compute residuals if m <= n
+ if residuals.size == 0:
+ computed_residuals = np.dot(A, t) - c
+ residuals_sum = np.sum(computed_residuals**2)
+ if residuals_sum < 1e-03:
+ return True, A.shape[1] - rank, t # Solution is valid
+ else:
+ logger.debug(f'{check_init_valid.__name__} failed with {residuals_sum=}')
+ return False, None, None # Solution is not valid
+ else:
+ if np.all(residuals < 1e-03):
+ return True, A.shape[1] - rank, t # Solution is valid
+ else:
+ logger.debug(f'{check_init_valid.__name__} failed with {residuals=}')
+ return False, None, None # No valid solution
+
+
+def project(points, plane_normal):
+ to_2D = trimesh.geometry.plane_transform(origin=(0,0,0), normal=plane_normal)
+ vertices_2D = trimesh.transformations.transform_points(points, to_2D)[:, :2]
+ return vertices_2D
+
+def apply_relations_surfacesample(
+ state: state_def.State,
+ name: str,
+):
+ obj_state = state.objs[name]
+ obj_name = obj_state.obj.name
+
+ parent_objs = []
+ parent_planes = []
+ obj_planes = []
+ margins = []
+ parent_tag_list = []
+
+ if len(obj_state.relations) == 0:
+ raise ValueError(f"Object {name} has no relations")
+ elif len(obj_state.relations) > 3:
+ raise ValueError(f"Object {name} has more than 2 relations, not supported. {obj_state.relations=}")
+
+ for i, relation_state in enumerate(obj_state.relations):
+
+ if isinstance(relation_state.relation, cl.AnyRelation):
+ raise ValueError(f"Got {relation_state.relation} for {name=} {relation_state.target_name=}")
+
+ parent_obj = state.objs[relation_state.target_name].obj
+ obj_plane, parent_plane = state.planes.get_rel_state_planes(state, name, relation_state)
+
+ if obj_plane is None:
+ continue
+ if parent_plane is None:
+ continue
+
+ obj_planes.append(obj_plane)
+ parent_planes.append(parent_plane)
+ parent_objs.append(parent_obj)
+ match relation_state.relation:
+ case cl.StableAgainst(child_tags, parent_tags, margin):
+ margins.append(margin)
+ parent_tag_list.append(parent_tags)
+ case cl.SupportedBy(child_tags, parent_tags):
+ margins.append(0)
+ parent_tag_list.append(parent_tags)
+ case _:
+ raise NotImplementedError
+
+ valid, dof, T = check_init_valid(state, name, obj_planes, parent_planes, margins)
+ if not valid:
+ rels = [(rels.relation, rels.target_name) for rels in obj_state.relations]
+ logger.warning(f'Init was invalid for {name=} {rels=}')
+ return None
+
+ if dof == 0:
+ iu.translate(state.trimesh_scene, obj_name, T)
+ elif dof == 1:
+
+ assert len(parent_planes) == 2, (name, len(parent_planes))
+
+ parent_obj1 = parent_objs[0]
+ parent_obj2 = parent_objs[1]
+ parent_plane1 = parent_planes[0]
+ parent_plane2 = parent_planes[1]
+ parent_tags1 = parent_tag_list[0]
+ parent_tags2 = parent_tag_list[1]
+ margin1 = margins[0]
+ margin2 = margins[1]
+ obj_plane1 = obj_planes[0]
+ obj_plane2 = obj_planes[1]
+
+ parent1_trimesh = state.planes.get_tagged_submesh(state.trimesh_scene, parent_obj1.name, parent_tags1, parent_plane1)
+ parent2_trimesh = state.planes.get_tagged_submesh(state.trimesh_scene, parent_obj2.name, parent_tags2, parent_plane2)
+
+ parent1_poly_index = parent_plane1[1]
+ parent1_poly = parent_obj1.data.polygons[parent1_poly_index]
+ plane_normal_1 = iu.global_polygon_normal(parent_obj1, parent1_poly)
+ pts = parent2_trimesh.vertices
+ projected = project(pts,plane_normal_1)
+ p1_to_p1 = trimesh.path.polygons.projected(parent1_trimesh, plane_normal_1, (0,0,0))
+
+ if p1_to_p1 is None:
+ raise ValueError(f'Failed to project {parent1_trimesh=} {plane_normal_1=} for {name=}')
+
+
+
+ if all([p1_to_p1.buffer(1e-1).contains(Point(pt[0], pt[1])) for pt in projected]):
+ face_mask = tagging.tagged_face_mask(parent_obj2, parent_tags2)
+ stability.move_obj_random_pt(state, obj_name, parent_obj2.name, face_mask, parent_plane2)
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj2.name, obj_plane2, parent_plane2, margin=margin2)
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj1.name, obj_plane1, parent_plane1, margin=margin1)
+ else:
+ face_mask = tagging.tagged_face_mask(parent_obj1, parent_tags1)
+ stability.move_obj_random_pt(state, obj_name, parent_obj1.name, face_mask, parent_plane1)
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj1.name, obj_plane1, parent_plane1, margin=margin1)
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj2.name, obj_plane2, parent_plane2, margin=margin2)
+
+ elif dof == 2:
+ assert len(parent_planes) == 1, (name, len(parent_planes))
+ for i, relation_state in enumerate(obj_state.relations):
+ parent_obj = state.objs[relation_state.target_name].obj
+ obj_plane, parent_plane = state.planes.get_rel_state_planes(state, name, relation_state)
+ if obj_plane is None:
+ continue
+ if parent_plane is None:
+ continue
+ iu.set_rotation(state.trimesh_scene, obj_name, (0, 0, 2*np.pi*np.random.randint(0, 4)/4))
+ face_mask = tagging.tagged_face_mask(parent_obj, relation_state.relation.parent_tags)
+ stability.move_obj_random_pt(state, obj_name, parent_obj.name, face_mask, parent_plane)
+ match relation_state.relation:
+ case cl.StableAgainst(child_tags, parent_tags, margin):
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj.name, obj_plane, parent_plane, margin=margin)
+ case cl.SupportedBy(child_tags, parent_tags):
+ stability.snap_against(state.trimesh_scene, obj_name, parent_obj.name, obj_plane, parent_plane, margin=0)
+ case _:
+ raise NotImplementedError
+ return parent_planes
+
+def validate_relations_feasible(state: state_def.State, name: str) -> bool:
+
+ assignments = state.objs[name].relations
+ targets = [rel.target_name for rel in assignments]
+
+ rooms = {targ for targ in targets if t.Semantics.Room in state.objs[targ].tags}
+ if len(rooms) > 1:
+ raise ValueError(f"Object {name} has multiple room targets {rooms}")
+
+@gin.configurable
+def try_apply_relation_constraints(
+ state: state_def.State,
+ name: str,
+ n_try_resolve=10,
+ visualize=False
+):
+
+ '''
+ name is in objs.name
+ name has been recently reassigned or added or swapped
+ it needs snapping, and dof updates
+
+ Result:
+ dof_mat and dof axis for name are updated
+ objstate for name has update location rotaton etc
+
+ '''
+
+ validate_relations_feasible(state, name)
+
+ for retry in range(n_try_resolve):
+ obj_state = state.objs[name]
+ if iu.blender_objs_from_names(obj_state.obj.name)[0].dimensions[2] > WALL_HEIGHT - WALL_THICKNESS:
+ logger.warning(f"Object {obj_state.obj.name} is too tall for the room: {obj_state.obj.dimensions[2]}, {WALL_HEIGHT=}, {WALL_THICKNESS=}")
+ parent_planes = apply_relations_surfacesample(state, name)
+
+
+ # assignments not valid
+ if parent_planes is None:
+ logger.debug(f'Found {parent_planes=} for {name=} {retry=}')
+ if visualize:
+ vis = butil.copy(obj_state.obj)
+ vis.name = obj_state.obj.name[:30] + '_noneplanes_' + str(retry)
+ return False
+
+ if validity.check_post_move_validity(state, name):
+ obj_state.dof_matrix_translation = combined_stability_matrix(parent_planes)
+ obj_state.dof_rotation_axis = combine_rotation_constraints(parent_planes)
+ return True
+
+ if visualize:
+ vis = butil.copy(obj_state.obj)
+ vis.name = obj_state.obj.name[:30] + '_failure_' + str(retry)
+
+ # butil.save_blend("test.blend")
+
+
+ logger.debug(f'Exhausted {n_try_resolve=} tries for {name=}')
+ return False
+
+
diff --git a/infinigen/core/constraints/example_solver/geometry/parse_scene.py b/infinigen/core/constraints/example_solver/geometry/parse_scene.py
new file mode 100644
index 000000000..11cfd8cf1
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/geometry/parse_scene.py
@@ -0,0 +1,79 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import bpy
+import trimesh
+from shapely import LineString, Point
+import numpy as np
+from typing import Union
+from trimesh import Trimesh, Scene
+from mathutils import Vector, Matrix
+
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints.constraint_language.util import (
+ translate,
+ rotate,
+ sync_trimesh
+)
+import fcl
+
+def to_trimesh(obj: bpy.types.Object):
+ bpy.context.view_layer.update()
+ verts = np.array([obj.matrix_world @ v.co for v in obj.data.vertices])
+ faces = np.array([p.vertices for p in obj.data.polygons])
+ mesh = trimesh.Trimesh(vertices=verts, faces=faces, process=False)
+ mesh.current_transform = trimesh.transformations.identity_matrix()
+ return mesh
+
+
+def preprocess_obj(obj):
+ with butil.ViewportMode(obj, mode='EDIT'):
+ butil.select(obj)
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+
+ bpy.context.view_layer.update()
+
+ butil.apply_transform(obj, loc=False, rot=False, scale=True)
+
+def preprocess_scene(objects):
+ for o in objects:
+ preprocess_obj(o)
+
+
+
+def parse_scene(objects):
+ # convert all bpy.objects into a trimesh.Scene
+
+ preprocess_scene(objects)
+
+ scene = trimesh.Scene()
+ for obj in objects:
+ add_to_scene(scene, obj)
+
+ return scene
+
+def add_to_scene(scene, obj, preprocess=True):
+ if preprocess:
+ preprocess_obj(obj)
+ obj_matrix_world = Matrix(obj.matrix_world)
+ obj.matrix_world = Matrix.Identity(4)
+ tmesh = to_trimesh(obj)
+ tmesh.metadata['tags'] = tagging.union_object_tags(obj)
+ scene.add_geometry(
+ geometry=tmesh,
+ # transform=np.array(obj.matrix_world),
+ geom_name=obj.name + '_mesh',
+ node_name=obj.name
+ )
+ col = trimesh.collision.CollisionManager()
+ T = trimesh.transformations.identity_matrix()
+ t = fcl.Transform(T[:3, :3], T[:3, 3])
+ tmesh.fcl_obj = col._get_fcl_obj(tmesh)
+ tmesh.col_obj = fcl.CollisionObject(tmesh.fcl_obj, t)
+ obj.matrix_world = obj_matrix_world
+ sync_trimesh(scene, obj.name)
+ return tmesh
diff --git a/infinigen/core/constraints/example_solver/geometry/planes.py b/infinigen/core/constraints/example_solver/geometry/planes.py
new file mode 100644
index 000000000..5e5cd1f9c
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/geometry/planes.py
@@ -0,0 +1,303 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+from __future__ import annotations
+import logging
+
+import bpy
+import numpy as np
+import gin
+import trimesh
+
+
+import infinigen.core.util.blender as butil
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints.constraint_language.util import meshes_from_names, blender_objs_from_names
+
+logger = logging.getLogger(__name__)
+
+def global_vertex_coordinates(obj, local_vertex):
+ return obj.matrix_world @ local_vertex.co
+
+def global_polygon_normal(obj, polygon):
+ loc, rot, scale = obj.matrix_world.decompose()
+ rot = rot.to_matrix()
+ normal = rot @ polygon.normal
+ try:
+ return normal / np.linalg.norm(normal)
+ except ZeroDivisionError:
+ raise ZeroDivisionError(f"Zero division error in global_polygon_normal for {obj.name=}, {polygon.index=}, {normal=}")
+
+class Planes:
+ def __init__(self):
+ self._mesh_hashes = {} # Dictionary to store mesh hashes for each object
+ self._cached_planes = {} # Dictionary to store computed planes, keyed by object and face_mask hash
+ self._cached_plane_masks = {} # Dictionary to store computed plane masks, keyed by object, plane, and face_mask hash
+
+ def calculate_mesh_hash(self, obj):
+ # Simple hash based on counts of vertices, edges, and polygons
+ mesh = obj.data
+ hash_str = f"{obj.name}_{len(mesh.vertices)}_{len(mesh.edges)}_{len(mesh.polygons)}"
+ return hash(hash_str)
+
+ def hash_face_mask(self, face_mask):
+ # Hash the face_mask to use as part of the key for caching
+ return hash(face_mask.tostring())
+
+ def get_all_planes_cached(self, obj, face_mask, tolerance=1e-4):
+ current_mesh_hash = self.calculate_mesh_hash(obj)
+ current_face_mask_hash = self.hash_face_mask(face_mask)
+ cache_key = (obj.name, current_face_mask_hash)
+
+ # Check if mesh has been modified or planes have not been computed before for this object and face_mask
+ if cache_key not in self._cached_planes or self._mesh_hashes.get(obj.name) != current_mesh_hash:
+ self._mesh_hashes[obj.name] = current_mesh_hash # Update the hash for this object
+ # Recompute planes for this object and face_mask and update cache
+ # logger.info(f'Cache MISS planes for {obj.name=}')
+ self._cached_planes[cache_key] = self.compute_all_planes_fast(obj, face_mask, tolerance)
+
+ # logger.info(f'Cache HIT planes for {obj.name=}')
+ return self._cached_planes[cache_key]
+
+
+ @staticmethod
+ def normalize(v):
+ norm = np.linalg.norm(v)
+ return v / norm if norm > 0 else v
+
+ @staticmethod
+ def hash_plane(normal, point, tolerance=1e-4):
+ normal_normalized = normal / np.linalg.norm(normal)
+ distance = np.dot(normal_normalized, point)
+ return (tuple(np.round(normal_normalized / tolerance).astype(int)), round(distance / tolerance))
+
+ def compute_all_planes_fast(self, obj, face_mask, tolerance=1e-4):
+ # Cache computations
+
+ vertex_cache = {v.index: global_vertex_coordinates(obj, v) for v in obj.data.vertices}
+ normal_cache = {p.index: global_polygon_normal(obj, p) for p in obj.data.polygons if face_mask[p.index]}
+
+ unique_planes = {}
+
+ for polygon in obj.data.polygons:
+ if not face_mask[polygon.index]:
+ continue
+
+ # Get the normal and a vertex to represent the plane
+ normal = normal_cache[polygon.index]
+
+ if np.linalg.norm(normal) < 1e-6:
+ continue
+
+ vertex = vertex_cache[polygon.vertices[0]]
+
+ # Hash the plane using both normal and the point
+ plane_hash = self.hash_plane(normal, vertex, tolerance)
+
+ if plane_hash not in unique_planes:
+ unique_planes[plane_hash] = (obj.name, polygon.index)
+
+ return list(unique_planes.values())
+
+
+ def get_all_planes_deprecated(self, obj, face_mask, tolerance=1e-4) -> tuple[str, int]:
+ "get all unique planes formed by faces in face_mask"
+ # ASSUMES: object is triangulated, no quads/polygons
+ unique_planes = []
+ for polygon in obj.data.polygons:
+ if not face_mask[polygon.index]:
+ continue
+ vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon.vertices[0]])
+ normal = global_polygon_normal(obj, polygon)
+ belongs_to_existing_plane = False
+ for name, polygon2_index in unique_planes:
+ polygon2 = obj.data.polygons[polygon2_index]
+ plane_vertex = global_vertex_coordinates(obj, obj.data.vertices[polygon2.vertices[0]])
+ plane_normal = global_polygon_normal(obj, polygon2)
+ if (
+ np.allclose(np.cross(normal, plane_normal), 0, rtol=tolerance) and
+ np.allclose(np.dot(vertex - plane_vertex, plane_normal), 0, rtol=tolerance)
+ ):
+ belongs_to_existing_plane = True
+ break
+ if not belongs_to_existing_plane and polygon.normal and polygon.normal.length > 0:
+ unique_planes.append((obj.name, polygon.index))
+ return unique_planes
+
+
+ @gin.configurable
+ def get_tagged_planes(self, obj: bpy.types.Object, tags: set, fast=True):
+ """
+ get all unique planes formed by faces tagged with tags
+ """
+
+ tags = t.to_tag_set(tags)
+
+ mask = tagging.tagged_face_mask(obj, tags)
+ if not mask.any():
+ obj_tags = tagging.union_object_tags(obj)
+ logger.warning(
+ f'Attempted to get_tagged_planes {obj.name=} {tags=} but mask was empty, {obj_tags=}'
+ )
+ return []
+
+ if fast:
+ planes = self.get_all_planes_cached(obj, mask)
+ else:
+ planes = self.compute_all_planes_fast(obj, mask)
+ return planes
+
+ def get_rel_state_planes(self, state, name: str, relation_state: tuple):
+
+ obj = state.objs[name].obj
+ relation = relation_state.relation
+
+ parent_obj = state.objs[relation_state.target_name].obj
+ obj_tags = relation.child_tags
+ parent_tags = relation.parent_tags
+
+ parent_all_planes = self.get_tagged_planes(parent_obj, parent_tags)
+ obj_all_planes = self.get_tagged_planes(obj, obj_tags)
+
+ #for i, p in enumerate(parent_all_planes):
+ # splitted_parent = planes.extract_tagged_plane(parent_obj, parent_tags, p)
+ # splitted_parent.name = f'parent_plane_{i}'
+ #for i, p in enumerate(obj_all_planes):
+ # splitted_parent = planes.extract_tagged_plane(parent_obj, obj_tags, p)
+ # splitted_parent.name = f'obj_plane_{i}'
+ #return
+
+ if relation_state.parent_plane_idx >= len(parent_all_planes):
+ logging.warning(f'{parent_obj.name=} had too few planes ({len(parent_all_planes)}) for {relation_state}')
+ parent_plane = None
+ else:
+ parent_plane = parent_all_planes[relation_state.parent_plane_idx]
+
+ if relation_state.child_plane_idx >= len(obj_all_planes):
+ logging.warning(f'{obj.name=} had too few planes ({len(obj_all_planes)}) for {relation_state}')
+ obj_plane = None
+ else:
+ obj_plane = obj_all_planes[relation_state.child_plane_idx]
+
+ return obj_plane, parent_plane
+
+ @staticmethod
+ def planerep_to_poly(planerep):
+ name, idx = planerep
+ return bpy.data.objects[name].data.polygons[idx]
+
+ def extract_tagged_plane(self, obj: bpy.types.Object, tags: set, plane: int):
+ """
+ get a single plane formed by faces tagged with tags
+ """
+
+ if obj.type != 'MESH':
+ raise TypeError("Object is not a mesh!")
+
+ face_mask = tagging.tagged_face_mask(obj, tags)
+ mask = self.tagged_plane_mask(obj, face_mask, plane)
+
+ if not mask.any():
+ obj_tags = tagging.union_object_tags(obj)
+ logger.warning(
+ f'Attempted to extract_tagged_plane {obj.name=} {tags=} but mask was empty, {obj_tags=}'
+ )
+
+ butil.select(obj)
+ bpy.context.view_layer.objects.active = obj
+
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ # Set initial selection for polygons to False
+ bpy.ops.object.mode_set(mode='OBJECT')
+
+ for poly in obj.data.polygons:
+ poly.select = mask[poly.index]
+
+ # Switch to Edit mode, duplicate the selection, and separate it
+ old_set = set(bpy.data.objects[:])
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.duplicate()
+ bpy.ops.mesh.separate(type='SELECTED')
+ bpy.ops.object.mode_set(mode='OBJECT')
+ new_set = set(bpy.data.objects[:]) - old_set
+ return new_set.pop()
+
+ def get_tagged_submesh(self, scene: trimesh.Scene, name:str, tags: set, plane: int):
+ obj = blender_objs_from_names(name)[0]
+ face_mask = tagging.tagged_face_mask(obj, tags)
+ mask = self.tagged_plane_mask(obj, face_mask, plane)
+ tmesh = meshes_from_names(scene, name)[0]
+ geom = tmesh.submesh(np.where(mask), append=True)
+ return geom
+
+ def tagged_plane_mask(self, obj: bpy.types.Object, face_mask: np.ndarray, plane: tuple[str, int], hash_tolerance=1e-4, plane_tolerance = 1e-2, fast = True) -> np.ndarray:
+ if not fast:
+ return self._compute_tagged_plane_mask(obj, face_mask, plane, plane_tolerance)
+ obj_id = obj.name
+ current_hash = self.calculate_mesh_hash(obj) # Calculate current mesh hash
+ face_mask_hash = self.hash_face_mask(face_mask) # Calculate hash for face_mask
+ ref_poly = self.planerep_to_poly(plane)
+ ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[ref_poly.vertices[0]])
+ ref_normal = global_polygon_normal(obj, ref_poly)
+ plane_hash = self.hash_plane(ref_normal, ref_vertex, hash_tolerance) # Calculate hash for plane
+
+ # Composite key now includes face_mask_hash
+ cache_key = (obj_id, plane_hash, face_mask_hash)
+
+ # Check if the mesh has been modified since last calculation or if the face mask has changed
+ mesh_or_face_mask_changed = cache_key not in self._cached_plane_masks or self._mesh_hashes.get(obj_id) != current_hash
+
+ if not mesh_or_face_mask_changed:
+ # logger.info(f'Cache HIT plane mask for {obj.name=}')
+ return self._cached_plane_masks[cache_key]['mask']
+
+ # If mesh or face mask changed, update the hash and recompute
+ self._mesh_hashes[obj_id] = current_hash
+
+ # Compute and cache the plane mask
+ # logger.info(f'Cache MISS plane mask for {obj.name=}')
+ plane_mask = self._compute_tagged_plane_mask(obj, face_mask, plane, plane_tolerance)
+
+ # Update the cache with the new result
+ self._cached_plane_masks[cache_key] = {
+ 'mask': plane_mask,
+ }
+
+ return plane_mask
+
+ def _compute_tagged_plane_mask(self, obj, face_mask, plane, tolerance):
+ """
+ Given a plane, return a mask of all polygons in obj that are coplanar with the plane.
+ """
+ plane_mask = np.zeros(len(obj.data.polygons), dtype=bool)
+ ref_poly = self.planerep_to_poly(plane)
+ ref_vertex = global_vertex_coordinates(obj, obj.data.vertices[ref_poly.vertices[0]])
+ ref_normal = global_polygon_normal(obj, ref_poly)
+
+ for candidate_polygon in obj.data.polygons:
+
+ if not face_mask[candidate_polygon.index]:
+ continue
+
+ candidate_vertex = global_vertex_coordinates(obj, obj.data.vertices[candidate_polygon.vertices[0]])
+ candidate_normal = global_polygon_normal(obj, candidate_polygon)
+ diff_vec = ref_vertex - candidate_vertex
+ if not np.isclose(np.linalg.norm(diff_vec), 0):
+ diff_vec /= np.linalg.norm(diff_vec)
+
+ ndot = np.dot(ref_normal, candidate_normal)
+ pdot = np.dot(diff_vec, candidate_normal)
+
+ in_plane = (
+ np.allclose(ndot, 1, atol=tolerance) and
+ np.allclose(pdot, 0, atol=tolerance)
+ )
+
+ plane_mask[candidate_polygon.index] = in_plane
+
+
+ return plane_mask
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py
new file mode 100644
index 000000000..c6a5c3b4a
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/geometry/stability.py
@@ -0,0 +1,358 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+from __future__ import annotations
+import logging
+from dataclasses import dataclass
+from copy import copy
+
+import numpy as np
+from shapely.affinity import rotate
+
+import bpy
+import trimesh
+from shapely.geometry import Point, LineString
+from shapely.ops import unary_union, nearest_points
+from shapely import Polygon
+from shapely import MultiPolygon
+import bmesh
+
+import matplotlib.pyplot as plt
+import gin
+# import fcl
+
+# from infinigen.core.util import blender as butil
+from infinigen.core.constraints.example_solver.geometry import planes as planes
+from infinigen.core.util import blender as butil
+from mathutils import Vector, Quaternion
+
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core.constraints import constraint_language as cl, reasoning as r
+
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints.constraint_language import util as iu
+
+import logging
+logger = logging.getLogger(__name__)
+
+def project_and_align_z_with_x(polygons, z_direction):
+ """
+ Rotate polygons so that the Z-direction is aligned with the X-axis in 2D.
+
+ Parameters:
+ polygons (list[Polygon]): List of Shapely Polygons representing the projected 2D polygons.
+ z_direction (np.array): The 2D direction vector where the Z-axis is projected.
+
+ Returns:
+ list[Polygon]: Rotated polygons with the Z-direction aligned with the X-axis.
+ """
+ # Calculate the angle between the Z-direction projection and the X-axis
+ angle_rad = np.arctan2(z_direction[1], z_direction[0])
+ angle_deg = np.degrees(angle_rad)
+
+ # Rotate polygons to align Z-direction with X-axis
+ rotated_polygons = [rotate(polygon, angle_deg, origin=(0, 0), use_radians=False) for polygon in polygons]
+
+ return rotated_polygons
+
+def is_vertically_contained(poly_a, poly_b):
+ """
+ Check if polygon A is vertically contained within polygon B, ignoring X-axis spillover.
+
+ Parameters:
+ poly_a (Polygon): Polygon A.
+ poly_b (Polygon): Polygon B.
+
+ Returns:
+ bool: True if A is vertically contained within B.
+ """
+ y_coords_a = [point[1] for point in poly_a.exterior.coords]
+ y_coords_b = [point[1] for point in poly_b.exterior.coords]
+
+ # Check vertical containment along the Y-axis
+ min_a, max_a = min(y_coords_a), max(y_coords_a)
+ min_b, max_b = min(y_coords_b), max(y_coords_b)
+
+ return min_b <= min_a and max_a <= max_b
+
+def project_vector(vector, origin, normal):
+ transform = trimesh.geometry.plane_transform(origin, normal)
+ transformed = trimesh.transformations.transform_points([np.array([0,0,0]), vector], transform)[:, :2]
+ transformed_vector = transformed[1] - transformed[0]
+ return transformed_vector
+
+@gin.configurable
+def stable_against(
+ state: state_def.State,
+ obj_name: str,
+ relation_state: state_def.RelationState,
+ visualize=False,
+ allow_overhangs=False
+):
+ """
+ check paralell, close to, and not overhanging.
+ """
+
+ relation = relation_state.relation
+ assert isinstance(relation, cl.StableAgainst)
+
+ logger.debug(f'stable against {obj_name=} {relation_state=}')
+ a_blender_obj = state.objs[obj_name].obj
+ b_blender_obj = state.objs[relation_state.target_name].obj
+ sa = state.objs[obj_name]
+ sb = state.objs[relation_state.target_name]
+
+ pa, pb = state.planes.get_rel_state_planes(state, obj_name, relation_state)
+
+ poly_a = state.planes.planerep_to_poly(pa)
+ poly_b = state.planes.planerep_to_poly(pb)
+
+ normal_a = iu.global_polygon_normal(a_blender_obj, poly_a)
+ normal_b = iu.global_polygon_normal(b_blender_obj, poly_b)
+ dot = np.array(normal_a).dot(normal_b)
+ if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)):
+ logger.debug(f'stable against failed, not parallel {dot=}')
+ return False
+
+ origin_b = iu.global_vertex_coordinates(b_blender_obj, b_blender_obj.data.vertices[poly_b.vertices[0]])
+
+ scene = state.trimesh_scene
+ a_trimesh = iu.meshes_from_names(scene, sa.obj.name)[0]
+ b_trimesh = iu.meshes_from_names(scene, sb.obj.name)[0]
+
+
+ mask = tagging.tagged_face_mask(sb.obj, relation.parent_tags)
+ mask = state.planes.tagged_plane_mask(sb.obj, mask, pb)
+ assert mask.any()
+ b_trimesh_mask = b_trimesh.submesh([np.where(mask)[0]], append=True)
+
+ # Project mesh A onto the plane of mesh B
+ projected_a = trimesh.path.polygons.projected(a_trimesh, normal_b, origin_b)
+ projected_b = trimesh.path.polygons.projected(b_trimesh_mask, normal_b, origin_b)
+ logger.debug(f'stable_against projecting along {normal_b} for parent_tags {relation.parent_tags}')
+
+
+ if projected_a is None or projected_b is None:
+ raise ValueError(f'Invalid {projected_a=} {projected_b=}')
+
+ if allow_overhangs:
+ res = projected_a.overlaps(projected_b)
+ elif relation.check_z:
+ res = projected_a.within(projected_b.buffer(1e-2))
+ else:
+ z_proj = project_vector(np.array([0, 0, 1]), origin_b, normal_b)
+ projected_a_rotated, projected_b_rotated = project_and_align_z_with_x([projected_a, projected_b], z_proj)
+ res = is_vertically_contained(projected_a_rotated, projected_b_rotated)
+
+ if visualize:
+ fig, ax = plt.subplots()
+ iu.plot_geometry(ax, projected_a, 'blue')
+ iu.plot_geometry(ax, projected_b, 'green')
+ plt.title(f'{obj_name} stable against {relation_state.target_name}? {res=}')
+ plt.show()
+
+ logger.debug(f'stable_against {res=}')
+ if not res:
+ return False
+
+ for vertex in poly_a.vertices:
+ vertex_global = iu.global_vertex_coordinates(a_blender_obj, a_blender_obj.data.vertices[vertex])
+ distance = iu.distance_to_plane(vertex_global, origin_b, normal_b)
+ if not np.isclose(distance, relation_state.relation.margin, atol=1e-2):
+ logger.debug(f'stable against failed, not close to {distance=}')
+ return False
+
+
+ return True
+
+def snap_against(scene, a, b, a_plane, b_plane, margin = 0):
+ """
+ snap a against b with some margin.
+ """
+ logging.debug("snap_against", a, b, a_plane, b_plane, margin)
+
+ a_obj = bpy.data.objects[a]
+ b_obj = bpy.data.objects[b]
+
+ a_poly_index = a_plane[1]
+ a_poly = a_obj.data.polygons[a_poly_index]
+ b_poly_index = b_plane[1]
+ b_poly = b_obj.data.polygons[b_poly_index]
+ plane_point_a = iu.global_vertex_coordinates(a_obj, a_obj.data.vertices[a_poly.vertices[0]])
+ plane_normal_a = iu.global_polygon_normal(a_obj, a_poly)
+ plane_point_b = iu.global_vertex_coordinates(b_obj, b_obj.data.vertices[b_poly.vertices[0]])
+ plane_normal_b = iu.global_polygon_normal(b_obj, b_poly)
+ plane_normal_b = -plane_normal_b
+
+
+
+ norm_mag_a = np.linalg.norm(plane_normal_a)
+ norm_mag_b = np.linalg.norm(plane_normal_b)
+ assert np.isclose(norm_mag_a, 1), norm_mag_a
+ assert np.isclose(norm_mag_b, 1), norm_mag_b
+
+ rotation_axis = np.cross(plane_normal_a, plane_normal_b)
+ if not np.isclose(np.linalg.norm(rotation_axis),0, atol = 1e-05):
+ rotation_axis = rotation_axis / np.linalg.norm(rotation_axis)
+ else:
+ rotation_axis = np.array([0,0,1])
+
+ dot = plane_normal_a.dot(plane_normal_b)
+ rotation_angle = np.arccos(np.clip(dot, -1, 1))
+ if np.isnan(rotation_angle):
+ raise ValueError(f'Invalid {rotation_angle=}')
+ iu.rotate(scene, a, rotation_axis, rotation_angle)
+
+
+
+ a_obj = bpy.data.objects[a]
+ a_poly = a_obj.data.polygons[a_poly_index]
+ # Recalculate vertex_a and normal_a after rotation
+ plane_point_a = iu.global_vertex_coordinates(a_obj, a_obj.data.vertices[a_poly.vertices[0]])
+ plane_normal_a = iu.global_polygon_normal(a_obj, a_poly)
+
+ distance = (plane_point_a - plane_point_b).dot(plane_normal_b)
+
+ # Move object a by the average distance minus the margin in the direction of the plane normal of b
+ translation = -(distance + margin) * plane_normal_b.normalized()
+ iu.translate(scene, a, translation)
+
+
+
+
+def random_sample_point(state: state_def.State, obj: bpy.types.Object, face_mask: np.ndarray, plane: tuple[str, int]) -> Vector:
+ """
+ Given a plane, return a random point on the plane.
+ """
+
+ if obj.type != 'MESH':
+ raise ValueError(f'Unexpected {obj.type=}')
+
+ plane_mask = state.planes.tagged_plane_mask(obj, face_mask, plane)
+ if not np.any(plane_mask):
+ logging.warning(
+ f'No faces in object {obj.name} are coplanar with plane {plane}.'
+ )
+
+ # Create a bmesh from the object mesh
+ bm = bmesh.new()
+ bm.from_mesh(obj.data)
+ bm.faces.ensure_lookup_table()
+
+ faces = [bm.faces[i] for i in np.where(plane_mask)[0]]
+
+ # Calculate the area for each face and create a cumulative distribution
+ areas = np.array([f.calc_area() for f in faces])
+ cumulative_areas = np.cumsum(areas)
+ total_area = cumulative_areas[-1]
+
+ # Generate a random number and find the corresponding face
+ random_area_point = np.random.rand() * total_area
+ face_index = np.searchsorted(cumulative_areas, random_area_point)
+ selected_face = faces[face_index]
+
+ verts = [v.co for v in selected_face.verts]
+
+ # Use barycentric coordinates to sample a random point in the triangle
+ # Random weights for each vertex
+ weights = np.random.rand(3)
+ weights /= np.sum(weights)
+ random_point_local = weights[0] * verts[0] + weights[1] * verts[1] + weights[2] * verts[2]
+ random_point_global = obj.matrix_world @ Vector(random_point_local)
+
+ bm.free()
+
+ return random_point_global
+
+def move_obj_random_pt(state: state_def.State, a, b, face_mask: np.ndarray, plane: tuple[str, int]):
+ """
+ move a to a random point on b
+ """
+ scene = state.trimesh_scene
+ b_obj = iu.blender_objs_from_names(b)[0]
+
+ random_point_global = random_sample_point(state, b_obj, face_mask, plane)
+ iu.set_location(scene, a, random_point_global)
+
+
+# def place_randomly(scene, a, b, visualize = False):
+# """
+# place a randomly on b.
+# """
+# a_blender_mesh = blender_objs_from_names(a)[0]
+# a_trimesh = meshes_from_names(scene, a)[0]
+# b_blender_mesh = blender_objs_from_names(b)[0]
+# b_trimesh = meshes_from_names(scene, b)[0]
+# b_proj = project_to_xy_poly(b_trimesh)
+
+# xy_loc = sample_random_point(b_proj)
+# if visualize:
+# fig, ax = plt.subplots()
+# if isinstance(b_proj, Polygon):
+# x, y = b_proj.exterior.xy
+# ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+# elif isinstance(b_proj, MultiPolygon):
+# for sub_poly in b_proj.geoms:
+# x, y = sub_poly.exterior.xy
+# ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+# ax.plot(xy_loc.x, xy_loc.y, 'o', color='black', label='Random point')
+# plt.show()
+
+# set_location(scene, a, Vector((xy_loc.x, xy_loc.y, 0)))
+
+def supported_by(scene, a, b, visualize = False):
+
+ #check for collision first
+
+
+ if isinstance(a, str):
+ a = [a]
+
+ a_meshes = iu.blender_objs_from_names(a)
+ a_trimeshes = iu.meshes_from_names(scene, a)
+ b_mesh = iu.blender_objs_from_names(b)[0]
+ b_trimesh = iu.meshes_from_names(scene, b)[0]
+
+ if visualize:
+ fig, ax = plt.subplots()
+ ax.set_aspect('equal', 'box')
+ b_poly = iu.project_to_xy_poly(b_trimesh)
+ if isinstance(b_poly, Polygon):
+ x, y = b_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+ elif isinstance(b_poly, MultiPolygon):
+ for sub_poly in b_poly.geoms:
+ x, y = sub_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+
+ for a_mesh, a_trimesh in zip(a_meshes, a_trimeshes):
+ cloned_a = butil.deep_clone_obj(
+ a_mesh, keep_modifiers=True, keep_materials=False
+ )
+ butil.modify_mesh(
+ cloned_a, "BOOLEAN", apply=True, operation="INTERSECT", object=b_mesh
+ )
+ iu.preprocess_obj(cloned_a)
+ intersection = iu.to_trimesh(cloned_a)
+ intersection_poly = iu.project_to_xy_poly(intersection)
+ intersection_convex = intersection_poly.convex_hull
+ com_projected = a_trimesh.centroid[:2]
+ if visualize:
+ if isinstance(intersection_poly, Polygon):
+ x, y = intersection_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+ elif isinstance(intersection_poly, MultiPolygon):
+ for sub_poly in intersection_poly.geoms:
+ x, y = sub_poly.exterior.xy
+ ax.fill(x, y, alpha=0.5, fc='blue', ec='black', label='Polygon a')
+ ax.plot(com_projected[0], com_projected[1], 'o', color='black', label='COM of a')
+
+ if not intersection_convex.contains(Point(com_projected)):
+ if visualize:
+ plt.show()
+ return False
+ if visualize:
+ plt.show()
+ return True
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/geometry/validity.py b/infinigen/core/constraints/example_solver/geometry/validity.py
new file mode 100644
index 000000000..c0761647f
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/geometry/validity.py
@@ -0,0 +1,122 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import logging
+
+import bpy
+from shapely.geometry import Point, Polygon, MultiPolygon
+
+from infinigen.core.util import blender as butil
+import infinigen.core.constraints.constraint_language as cl
+from infinigen.core.constraints.constraint_language.util import meshes_from_names, blender_objs_from_names, subset, project_to_xy_poly
+from infinigen.core.constraints.example_solver.state_def import State, ObjectState, RelationState
+from infinigen.core.constraints.evaluator.node_impl.trimesh_geometry import constrain_contact, any_touching
+from infinigen.core.constraints.example_solver.geometry.stability import stable_against, supported_by
+
+from infinigen.core import tags as t
+
+import gin
+
+logger = logging.getLogger(__name__)
+
+def check_pre_move_validity(scene, a, parent_dict, dx, dy):
+ """
+ """
+ parent = parent_dict[a]
+ a_mesh = meshes_from_names(scene, a)[0]
+ parent_mesh = meshes_from_names(scene, parent)[0]
+ blender_mesh = blender_objs_from_names(a)[0]
+
+
+ # move a mesh by dx, dy and check if the projection of a_mesh is contained in parent_mesh
+ # a_mesh.apply_transform(trimesh.transformations.compose_matrix(translate=[dx,dy,0]))
+ a_poly = project_to_xy_poly(a_mesh)
+ parent_poly = project_to_xy_poly(parent_mesh)
+ centroid = a_poly.centroid
+ new_centroid = Point([centroid.x + dx, centroid.y + dy])
+ # plot
+ # fig, ax = plt.subplots()
+ # if isinstance(parent_poly, Polygon):
+ # x, y = parent_poly.exterior.xy
+ # ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+ # elif isinstance(parent_poly, MultiPolygon):
+ # for sub_poly in parent_poly.geoms:
+ # x, y = sub_poly.exterior.xy
+ # ax.fill(x, y, alpha=0.5, fc='red', ec='black', label='Polygon b')
+ # ax.plot(centroid.x, centroid.y, 'o', color='black', label='Random point')
+ # plt.show()
+ # scene.show()
+
+ if isinstance(a_poly, Polygon):
+ if not parent_poly.contains(new_centroid):
+ # print("not contained")
+ return False
+ elif isinstance(a_poly, MultiPolygon):
+ for sub_poly in a_poly.geoms:
+ if not parent_poly.contains(new_centroid):
+ # print("not contained")
+ return False
+
+ return True
+
+def all_relations_valid(state, name):
+
+ rels = state.objs[name].relations
+ for i, relation_state in enumerate(rels):
+ match relation_state.relation:
+ case cl.StableAgainst(child_tags, parent_tags, margin):
+ res = stable_against(state, name, relation_state)
+ if not res:
+ logger.debug(f'{name} failed relation {i=}/{len(rels)} {relation_state.relation} on {relation_state.target_name}')
+ return False
+ case unmatched:
+ raise TypeError(f"Unhandled {relation_state.relation}")
+
+ return True
+
+@gin.configurable
+def check_post_move_validity(
+ state: State,
+ name: str,
+ disable_collision_checking=False,
+ visualize=False
+):
+
+ scene = state.trimesh_scene
+ objstate = state.objs[name]
+
+ collision_objs = [
+ os.obj.name for k, os in state.objs.items()
+ if k != name and t.Semantics.NoCollision not in os.tags
+ ]
+
+ if len(collision_objs) == 0:
+ return True
+
+ if not all_relations_valid(state, name):
+
+ if visualize:
+ vis_obj = butil.copy(objstate.obj)
+ vis_obj.name = f'validity_relations_fail_{name}'
+
+ return False
+
+ if disable_collision_checking:
+ return True
+ if t.Semantics.NoCollision in objstate.tags:
+ return True
+
+ touch = any_touching(scene, objstate.obj.name, collision_objs, bvh_cache=state.bvh_cache)
+ if not constrain_contact(touch, should_touch=None, max_depth=0.0001):
+ if visualize:
+ vis_obj = butil.copy(objstate.obj)
+ vis_obj.name = f'validity_contact_fail_{name}'
+
+ contact_names = [[x for x in t.names if not x.startswith('_')] for t in touch.contacts]
+ logger.debug(f'validity failed - {name} touched {contact_names[0]} {len(contact_names)=}')
+ return False
+
+ # supposed to go through the consgraph here
+ return True
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/greedy/__init__.py b/infinigen/core/constraints/example_solver/greedy/__init__.py
new file mode 100644
index 000000000..f83271319
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/greedy/__init__.py
@@ -0,0 +1,3 @@
+from .all_substitutions import substitutions, iterate_assignments
+from .constraint_partition import filter_constraints
+from .active_for_stage import update_active_flags, set_active
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/greedy/active_for_stage.py b/infinigen/core/constraints/example_solver/greedy/active_for_stage.py
new file mode 100644
index 000000000..3db38b1e9
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/greedy/active_for_stage.py
@@ -0,0 +1,113 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import logging
+
+from infinigen.core import tags as t
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+)
+from infinigen.core.constraints.evaluator import domain_contains
+from infinigen.core.constraints.example_solver import state_def
+
+from infinigen.core.util import blender as butil
+
+logger = logging.getLogger(__name__)
+
+def find_ancestors_of_type(
+ state: state_def.State,
+ objkey: str,
+ filter_type: r.Domain,
+ seen: set = None
+) -> set[str]:
+
+ """
+ Find objkeys of all ancestors of `objkey` which match `filter_type`
+
+ Object `A` is a parent of `objkey` if there exists a sequence of objects `A, B, C, ..., objkey`
+ where A is in B's relations, B is in C's relations, etc, AND only `A` matches the filter
+
+ Returns
+ -------
+ parents: set(str)
+ objkeys of objects of the given type which are parents in the relation graph
+
+ """
+
+ if seen is None:
+ seen = set()
+
+ seen.add(objkey)
+
+ obj = state.objs[objkey]
+
+ if domain_contains.domain_contains(filter_type, state, obj):
+ return {objkey}
+
+ result = set()
+ for rel in obj.relations:
+
+ if rel.target_name in seen:
+ continue
+
+ result.update(find_ancestors_of_type(
+ state, rel.target_name, filter_type, seen
+ ))
+
+ return result
+
+def _is_active_room_object(
+ state: state_def.State,
+ objkey: str,
+ var_assignments: dict[t.Variable, str]
+) -> bool:
+
+ """
+ Determine if an object should be active for the given assignment
+
+ if there is a `room` var specified, `objkey` must be a descendent of that room
+ if there is an `obj` var specified, `objkey must not be a descendent of any other obj
+
+ """
+
+ for var, assignment in var_assignments.items():
+ if assignment is None:
+ continue
+ match var.name:
+ case 'room':
+ room_ancestors = find_ancestors_of_type(state, objkey, r.Domain({t.Semantics.Room}))
+ if assignment not in room_ancestors:
+ logger.debug(f'{objkey} is inactive due to room {room_ancestors=} {assignment=}')
+ return False
+ case 'obj':
+ obj_ancestors = find_ancestors_of_type(state, objkey, r.Domain({t.Semantics.Object}))
+ if len(obj_ancestors) and objkey not in obj_ancestors:
+ logger.debug(f'{objkey} is inactive due to obj {assignment=} {obj_ancestors=}')
+ return False
+ case _:
+ raise NotImplementedError(
+ f"{_is_active_room_object.__name__} encountered unknown variable {var}. "
+ "Greedy stages with vars besides room/obj are not yet supported"
+ )
+
+ return True
+
+def set_active(state, objkey, active):
+ state.objs[objkey].active = active
+ for child in butil.iter_object_tree(state.objs[objkey].obj):
+ child.hide_viewport = not active
+
+def update_active_flags(
+ state: state_def.State,
+ var_assignments: dict[t.Variable, str]
+):
+ count = 0
+ for objkey, objstate in state.objs.items():
+ active = _is_active_room_object(state, objkey, var_assignments)
+ set_active(state, objkey, active)
+ count += active
+ return count
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/greedy/all_substitutions.py b/infinigen/core/constraints/example_solver/greedy/all_substitutions.py
new file mode 100644
index 000000000..c5237f390
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/greedy/all_substitutions.py
@@ -0,0 +1,183 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+import itertools
+import copy
+import logging
+import functools
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r
+)
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core import tags as t
+from infinigen.core.constraints.evaluator.domain_contains import objkeys_in_dom
+
+logger = logging.getLogger(__name__)
+
+def _resolve_toplevel_var(
+ dom: r.Domain,
+ state: state_def.State,
+ limits: dict[t.Variable, int] = None,
+) -> typing.Iterator[str]:
+
+ """
+ Find and yield all valid substitutions of a toplevel VariableTag in a given dom
+
+ ASSUMES: there is at most one variable in the domain, and it is at the top level
+ """
+
+ if limits is None:
+ limits = {}
+
+ vars = [ti for ti in dom.tags if isinstance(ti, t.Variable)]
+ if len(vars) == 0:
+ yield dom
+ return
+ elif len(vars) > 1:
+ raise ValueError(f"More than one variable in domain {dom}")
+
+ # valid assignments for the var are any objs satisfying everything else in the domain
+ vartag = vars[0]
+ result = copy.deepcopy(dom)
+ result.tags.remove(vartag)
+ objkeys = objkeys_in_dom(result, state)
+ logger.debug(f'Found {len(objkeys)} valid assignments for {repr(vartag)} via {result} on ')
+
+ # if the user says limit "room" to 3 and we are doing "room", apply the limit
+ name_limit = limits.get(vartag, None)
+ if name_limit is not None:
+ objkeys = objkeys[:name_limit]
+
+ for objkey in objkeys:
+ logger.debug(f'Assigning {objkey} for {vartag}')
+ yield result.with_tags(state.objs[objkey].tags)
+
+def substitutions(
+ dom: r.Domain,
+ state: state_def.State,
+ limits: dict[t.Variable, int] | None = None,
+ nonempty: bool = False,
+) -> typing.Iterator[r.Domain]:
+
+ """Find all t.Variable in d's tags or relations, and return one Domain for each possible assignment
+
+ limits cuts off enumeration of each varname with some integer count
+ """
+
+ child_assignment_prod = itertools.product(*(
+ substitutions(dchild, state, limits, nonempty)
+ for _, dchild in dom.relations
+ ))
+
+ i = None
+
+ for i, dsubs in enumerate(child_assignment_prod):
+
+ assert len(dsubs) == len(dom.relations)
+ rels = [(rel, dsubs[j]) for j, (rel, _) in enumerate(dom.relations)]
+
+ candidate = r.Domain(
+ tags=dom.tags, relations=rels
+ )
+
+ yield from _resolve_toplevel_var(candidate, state, limits=limits)
+
+ if i is None and nonempty:
+ raise ValueError(f'Found no substitutions found for {dom=}')
+
+def iterate_assignments(
+ dom: r.Domain,
+ state: state_def.State,
+ vars: list[t.Variable],
+ limits: dict[t.Variable, int] | None = None,
+ nonempty: bool = False,
+) -> typing.Iterator[dict[t.Variable, str]]:
+
+ """Find all combinations of assignments for the listed vars.
+
+ Variables will be considered IN ORDER, IE first variable can affect options for second variable,
+ but not the other way around.
+
+ Parameters
+ ----------
+ dom : r.Domain
+ The domain to substitute variables in
+ state : state_def.State
+ The state to substitute variables in
+ vars : list[str]
+ The names of the variables to substitute
+ limits : dict[str, int]
+ Consider only the first N objects for each variable
+ nonempty : bool
+ Raise an error if no substitutions are found
+
+ Returns
+ -------
+ typing.Iterator[dict[str, r.Domain]]
+ Iterator over dicts of variable assignments to domains
+
+ """
+
+ if limits is None:
+ limits = {}
+
+ if len(vars) == 0:
+ yield {}
+ return
+
+ assert isinstance(vars, list), vars
+ var = vars[0]
+
+ doms_for_var = [
+ d for d in dom.traverse() if var in d.tags
+ ]
+ if len(doms_for_var) == 0:
+ yield {}
+ return
+
+ combined, *rest = doms_for_var
+ for d in rest:
+ combined = combined.intersection(d)
+ combined = copy.deepcopy(combined) # prevents modification of original domain if it had the var
+ combined.tags.remove(var)
+ if not combined.intersects(combined):
+ raise ValueError(f'{iterate_assignments.__name__} with {var=} arrived at contradictory {combined=}')
+
+ candidates = sorted(objkeys_in_dom(combined, state))
+
+ candidates = [
+ c for c in candidates
+ if t.Semantics.NoChildren not in state.objs[c].tags
+ ]
+
+ i = None
+ for i, objkey in enumerate(candidates):
+
+ limit = limits.get(var, None)
+ if limit is not None and i >= limits[var]:
+ break
+
+ dom_objkey = r.domain_tag_substitute(
+ copy.deepcopy(dom), var, combined.with_tags(t.SpecificObject(objkey))
+ )
+ rest_iter = iterate_assignments(
+ dom_objkey, state, vars[1:], limits,
+ )
+
+ for rest_assignments in rest_iter:
+ yield {
+ var: objkey,
+ **rest_assignments
+ }
+
+ if i is None and nonempty:
+ raise ValueError(f'Found no assignments found for {dom=}')
+
+
+
diff --git a/infinigen/core/constraints/example_solver/greedy/constraint_partition.py b/infinigen/core/constraints/example_solver/greedy/constraint_partition.py
new file mode 100644
index 000000000..1511f9765
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/greedy/constraint_partition.py
@@ -0,0 +1,269 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+import operator
+import copy
+from functools import partial
+from dataclasses import dataclass
+import logging
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ example_solver as ex,
+ reasoning as r,
+)
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+OPS_COMMUTATIVE = {
+ operator.add,
+ operator.and_,
+ operator.mul,
+ operator.or_
+}
+
+OPS_UNIT_VALUE = {
+ operator.add: 0,
+ operator.mul: 1,
+ operator.pow: 0,
+ operator.truediv: 0
+}
+
+def _get_op_unit_value(node: cl.BoolExpression | cl.ScalarExpression):
+ match node:
+ case cl.BoolOperatorExpression(func, operands):
+ return True
+ case cl.ScalarOperatorExpression(func, operands) if func in OPS_UNIT_VALUE:
+ return OPS_UNIT_VALUE[func]
+ case _:
+ raise ValueError(f'Found no unit value for {node.__class__} {node.func}')
+
+def _partition_dict(
+ terms: dict[str, cl.Node],
+ recurse: typing.Callable
+):
+ new_terms = {}
+ for k, v in terms.items():
+ part, relevant = recurse(v)
+ if not relevant:
+ continue
+ new_terms[k] = part
+ return new_terms
+
+def _update_item_nodes(
+ node: cl.Node,
+ from_varname: str,
+ to_varname: str,
+ to_objs: cl.ObjectSetExpression
+):
+ for child in node.traverse():
+ if not isinstance(child, cl.item):
+ continue
+ if child.name != from_varname:
+ continue
+ child.name = to_varname
+ child.member_of = to_objs
+
+ return node
+
+def _filter_gather_constraint(
+ node: cl.ForAll | cl.SumOver | cl.MeanOver,
+ recurse: typing.Callable,
+ filter_dom: r.Domain,
+ var_assignments: dict[t.Variable, r.Domain]
+) -> tuple[cl.Node, bool]:
+
+ objs, var, pred = node.objs, node.var, node.pred
+ var = t.Variable(var)
+
+ objs_part, objs_rel = recurse(objs)
+
+ obj_dom = r.constraint_domain(objs)
+ obj_dom = r.substitute_all(obj_dom, var_assignments)
+
+ var_assignments = copy.deepcopy(var_assignments) or {}
+ for varname, dom in var_assignments.items():
+ assert isinstance(varname, t.Variable)
+ var_assignments[varname] = r.domain_tag_substitute(dom, var, obj_dom)
+ var_assignments[var] = obj_dom
+
+ pred_part, pred_rel = recurse(pred, var_assignments=var_assignments)
+
+ for pred_child in pred_part.traverse():
+ if not isinstance(pred_child, r.FilterByDomain):
+ continue
+ subst_filter, matched = r.domain_tag_substitute(
+ pred_child.filter, var, obj_dom, return_match=True
+ )
+
+ pred_child.filter = subst_filter
+
+ res = copy.copy(node)
+ res.objs = objs_part
+ res.var = var
+ res.pred = pred_part
+
+ relevant = pred_rel
+ return res, relevant
+
+def _filter_object_set(
+ node: cl.ObjectSetExpression,
+ recurse: typing.Callable,
+ filter_dom: r.Domain,
+ var_assignments: dict[t.Variable, r.Domain],
+) -> tuple[cl.Node, bool]:
+
+ new_consnode = copy.deepcopy(node)
+
+ dom = r.constraint_domain(node)
+ dom_subst = r.substitute_all(dom, var_assignments)
+
+ if not r.domain_finalized(dom_subst, check_anyrel=False, check_variable=True):
+ raise ValueError(
+ "Domain not finalized, unable to check against filter. "
+ "Check for any undefined variables? should be impossible. "
+ f"{dom=}."
+ )
+
+ relevant = dom_subst.intersects(filter_dom, require_satisfies_right=True)
+ if (
+ relevant
+ and not dom_subst.satisfies(filter_dom) # no need to filter something that is already strict enough
+ ):
+ finalized = r.domain_finalized(filter_dom, check_anyrel=False, check_variable=True)
+ assert finalized, filter_dom
+ new_consnode = r.FilterByDomain(new_consnode, filter_dom)
+
+ return new_consnode, relevant
+
+def _filter_operator(
+ node: cl.BoolOperatorExpression | cl.ScalarOperatorExpression,
+ recurse: typing.Callable,
+ filter_dom: r.Domain,
+ var_assignments: dict[t.Variable, r.Domain]
+) -> tuple[cl.Node, bool]:
+
+ operands, func = node.operands, node.func
+
+ op_results = [recurse(o) for o in operands]
+ relevant_ops = [node for node, rel in op_results if rel]
+
+ match relevant_ops, func:
+ case ([], _):
+ return cl.constant(_get_op_unit_value(node)), False
+ case ([op], f) if f in OPS_COMMUTATIVE:
+ return op, True
+ case (new_operands, f) if (
+ len(new_operands) == len(operands)
+ or f in OPS_COMMUTATIVE
+ ):
+ return node.__class__(f, new_operands), True
+ case _:
+ res = node.__class__(func, [o[0] for o in op_results])
+ any_relevant = any(o[1] for o in op_results)
+ return res, any_relevant
+
+def _filter_node_cases(
+ node: cl.Node,
+ recurse: typing.Callable,
+ filter_dom: r.Domain,
+ var_assignments: dict[t.Variable, r.Domain]
+) -> tuple[cl.Node, bool]:
+ match node:
+ case cl.Problem(cons, score_terms):
+ prob = cl.Problem(
+ _partition_dict(cons, recurse),
+ _partition_dict(score_terms, recurse)
+ )
+ relevant = len(prob.constraints) > 0 or len(prob.score_terms) > 0
+ return prob, relevant
+ case cl.ForAll() | cl.SumOver() | cl.MeanOver():
+ return _filter_gather_constraint(node, recurse, filter_dom, var_assignments)
+ case cl.BoolOperatorExpression() | cl.ScalarOperatorExpression():
+ return _filter_operator(node, recurse, filter_dom, var_assignments)
+ case cl.ObjectSetExpression():
+ return _filter_object_set(node, recurse, filter_dom, var_assignments)
+ case _:
+
+ result_relevant = False
+ result_consnode = copy.deepcopy(node)
+
+ for name, child in node.children():
+ res, relevant = recurse(child)
+ if not hasattr(node, name):
+ raise ValueError(f"Node {node.__class__} has child with {name=} but no attribute {name} to set")
+ setattr(result_consnode, name, res)
+ result_relevant = result_relevant or relevant
+
+ return result_consnode, result_relevant
+
+def _check_partition_correctness(
+ node: cl.ObjectSetExpression,
+ filter_dom: r.Domain,
+ var_assignments: dict[t.Variable, r.Domain],
+):
+ res_dom = r.constraint_domain(node)
+ res_dom = r.substitute_all(res_dom, var_assignments)
+
+ if not r.domain_finalized(res_dom, check_anyrel=False, check_variable=True):
+ raise ValueError(
+ f"While doing {_check_partition_correctness.__name__} for {node=} {filter_dom=}, "
+ f"got {res_dom=} is not finalized, {var_assignments.keys()=}"
+ )
+
+ if not res_dom.satisfies(filter_dom):
+ raise ValueError(f"{res_dom=} does not satisfy {filter_dom=}")
+
+def filter_constraints(
+ node: cl.Node,
+ filter_dom: r.Domain,
+ var_assignments: dict[str, r.Domain] = None,
+ check_correctness=True
+) -> tuple[cl.Node, bool]:
+
+ """ Return a constraint graph representing the component of `node` that is relevant for
+ to a particular greedy filter domain.
+
+ Parameters
+ ----------
+ node : cl.Node
+ The constraint program to partition
+ filter_dom : Domain
+ The domain which determines whether a constraint is relevant
+ var_assignments : Domain
+ Domains to substitute for any t.Variable(name: str) in the constraint program, typically used for recursive calls.
+
+ Returns
+ -------
+ partitioned: cl.Node
+ The partitioned constraint program
+ relevant: bool
+ Was any part of the constraint program relevant?
+
+ """
+
+ assert isinstance(node, cl.Node), node
+
+ if var_assignments is None:
+ var_assignments = {}
+
+ recurse = partial(filter_constraints, filter_dom=filter_dom, var_assignments=var_assignments)
+
+ logger.debug(f"{filter_constraints.__name__} for {node.__class__.__name__}, {var_assignments.keys()=}")
+ new_node, relevant = _filter_node_cases(node, recurse, filter_dom, var_assignments)
+
+ if (
+ relevant
+ and check_correctness
+ and isinstance(new_node, cl.ObjectSetExpression)
+ ):
+ _check_partition_correctness(new_node, filter_dom, var_assignments)
+
+ logger.debug(f"Partitioned {node.__class__.__name__} to {new_node.__class__.__name__}")
+
+ return new_node, relevant
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/__init__.py b/infinigen/core/constraints/example_solver/moves/__init__.py
new file mode 100644
index 000000000..1e0feb4ed
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/__init__.py
@@ -0,0 +1,6 @@
+from .moves import Move
+from .addition import Addition, Resample
+from .deletion import Deletion
+from .swap import Swap
+from .reassignment import RelationPlaneChange, RelationTargetChange
+from .pose import TranslateMove, RotateMove, ReinitPoseMove
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/addition.py b/infinigen/core/constraints/example_solver/moves/addition.py
new file mode 100644
index 000000000..26326a017
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/addition.py
@@ -0,0 +1,188 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from dataclasses import dataclass, field
+import numpy as np
+import typing
+import logging
+import gin
+
+from pprint import pprint
+
+import bpy
+from mathutils import Vector, Matrix
+import trimesh
+
+from infinigen.assets.utils import bbox_from_mesh
+
+from infinigen.core.constraints.example_solver.state_def import State, ObjectState
+from infinigen.core import tagging, tags as t
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ usage_lookup
+)
+
+from infinigen.core.util import blender as butil
+from infinigen.core.constraints.constraint_language.util import (
+ delete_obj,
+ meshes_from_names
+)
+from infinigen.core.constraints.example_solver.geometry import(
+ validity
+)
+
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.nodes import node_utils
+from infinigen.core.placement.factory import AssetFactory
+
+from infinigen.core.constraints.example_solver.geometry import dof, parse_scene
+from . import moves
+from .reassignment import pose_backup, restore_pose_backup
+from time import time
+# from line_profiler import LineProfiler
+
+
+logger = logging.getLogger(__name__)
+
+GLOBAL_GENERATOR_SINGLETON_CACHE = {}
+
+def sample_rand_placeholder(gen_class: type[AssetFactory]):
+
+ singleton_gen = usage_lookup.has_usage(gen_class, t.Semantics.SingleGenerator)
+
+ if singleton_gen and gen_class in GLOBAL_GENERATOR_SINGLETON_CACHE:
+ gen = GLOBAL_GENERATOR_SINGLETON_CACHE[gen_class]
+ else:
+ fac_seed = np.random.randint(1e7)
+ gen = gen_class(fac_seed)
+ if singleton_gen:
+ GLOBAL_GENERATOR_SINGLETON_CACHE[gen_class] = gen
+
+ inst_seed = np.random.randint(1e7)
+
+ if usage_lookup.has_usage(gen_class, t.Semantics.RealPlaceholder):
+ new_obj = gen.spawn_placeholder(inst_seed, loc=(0,0,0), rot=(0,0,0))
+ elif usage_lookup.has_usage(gen_class, t.Semantics.AssetAsPlaceholder):
+ new_obj = gen.spawn_asset(inst_seed, loc=(0,0,0), rot=(0,0,0))
+ elif usage_lookup.has_usage(gen_class, t.Semantics.PlaceholderBBox):
+ new_obj = bbox_from_mesh.bbox_mesh_from_hipoly(gen, inst_seed, use_pholder=True)
+ else:
+ new_obj = bbox_from_mesh.bbox_mesh_from_hipoly(gen, inst_seed)
+
+ if new_obj.type != 'MESH':
+ raise ValueError(f'Addition created {new_obj.name=} with type {new_obj.type}')
+ if len(new_obj.data.polygons) == 0:
+ raise ValueError(f'Addition created {new_obj.name=} with 0 faces')
+
+ butil.put_in_collection(
+ list(butil.iter_object_tree(new_obj)),
+ butil.get_collection(f'placeholders')
+ )
+ parse_scene.preprocess_obj(new_obj)
+ tagging.tag_canonical_surfaces(new_obj)
+
+ return new_obj, gen
+
+@dataclass
+class Addition(moves.Move):
+
+ """ Move which generates an object and adds it to the scene with certain relations
+ """
+
+ gen_class: typing.Any
+ relation_assignments: list
+ temp_force_tags: set
+
+ _new_obj: bpy.types.Object = None
+
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({self.gen_class.__name__}, {len(self.relation_assignments)} relations)'
+
+ def apply(self, state: State):
+
+ target_name, = self.names
+ assert target_name not in state.objs
+
+ self._new_obj, gen = sample_rand_placeholder(self.gen_class)
+
+ parse_scene.add_to_scene(state.trimesh_scene, self._new_obj, preprocess=True)
+
+ tags = self.temp_force_tags.union(usage_lookup.usages_of_factory(gen.__class__))
+
+ assert isinstance(self._new_obj, bpy.types.Object)
+ objstate = ObjectState(
+ obj=self._new_obj,
+ generator=gen,
+ tags=tags,
+ relations=self.relation_assignments
+ )
+
+ state.objs[target_name] = objstate
+ success = dof.try_apply_relation_constraints(state, target_name)
+ logger.debug(f'{self} {success=}')
+ return success
+
+ def revert(self, state: State):
+ to_delete = list(butil.iter_object_tree(self._new_obj))
+ delete_obj(state.trimesh_scene, [a.name for a in to_delete])
+
+ new_name, = self.names
+ del state.objs[new_name]
+
+@dataclass
+class Resample(moves.Move):
+
+ """ Move which replaces an existing object with a new one from the same generator
+ """
+
+ align_corner: int = None
+
+ _backup_gen = None
+ _backup_obj = None
+ _backup_poseinfo = None
+
+ def apply(self, state: State):
+
+ assert len(self.names) == 1
+ target_name = self.names[0]
+
+ os = state.objs[target_name]
+ self._backup_gen = os.generator
+ self._backup_obj = os.obj
+ self._backup_poseinfo = pose_backup(os)
+
+ scene = state.trimesh_scene
+ scene.graph.transforms.remove_node(os.obj.name)
+ scene.delete_geometry(os.obj.name + '_mesh')
+
+ os.obj, os.generator = sample_rand_placeholder(os.generator.__class__)
+
+ if self.align_corner is not None:
+ c_old = self._backup_obj.bound_box[self.align_corner]
+ c_new = os.obj.bound_box[self.align_corner]
+ raise NotImplementedError(f'{self.align_corner=}')
+
+ parse_scene.add_to_scene(state.trimesh_scene, os.obj, preprocess=True)
+ dof.apply_relations_surfacesample(state, target_name)
+
+ return validity.check_post_move_validity(state, target_name)
+
+ def revert(self, state: State):
+
+ target_name, = self.names
+
+ os = state.objs[target_name]
+ delete_obj(state.trimesh_scene, os.obj.name)
+
+ os.obj = self._backup_obj
+ os.generator = self._backup_gen
+ parse_scene.add_to_scene(state.trimesh_scene, os.obj, preprocess=False)
+ restore_pose_backup(state, target_name, self._backup_poseinfo)
+
+ def accept(self, state: State):
+ butil.delete(list(butil.iter_object_tree(self._backup_obj)))
diff --git a/infinigen/core/constraints/example_solver/moves/deletion.py b/infinigen/core/constraints/example_solver/moves/deletion.py
new file mode 100644
index 000000000..58097bc5c
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/deletion.py
@@ -0,0 +1,52 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from dataclasses import dataclass
+import numpy as np
+import typing
+import logging
+
+import bpy
+from infinigen.core.constraints.example_solver.geometry import parse_scene
+from mathutils import Vector, Matrix
+import trimesh
+
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core.util import blender as butil
+
+from infinigen.core.constraints.example_solver.moves import Move
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class Deletion(Move):
+
+ # remove obj from scene
+ _backup_state: state_def.ObjectState = None
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({self.names})'
+
+ def apply(self, state):
+
+ target_name, = self.names
+ self._backup_state = state.objs[target_name]
+
+ for obj in butil.iter_object_tree(state.objs[target_name].obj):
+ state.trimesh_scene.graph.transforms.remove_node(obj.name)
+ state.trimesh_scene.delete_geometry(obj.name + '_mesh')
+
+ del state.objs[target_name]
+ return True
+
+ def accept(self, state):
+ butil.delete(list(butil.iter_object_tree(self._backup_state.obj)))
+
+ def revert(self, state):
+ target_name, = self.names
+ state.objs[target_name] = self._backup_state
+ parse_scene.add_to_scene(state.trimesh_scene, self._backup_state.obj, preprocess=True)
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/moves.py b/infinigen/core/constraints/example_solver/moves/moves.py
new file mode 100644
index 000000000..ada8cb2c0
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/moves.py
@@ -0,0 +1,37 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from dataclasses import dataclass
+import numpy as np
+import typing
+import logging
+
+import bpy
+from infinigen.core.constraints.example_solver.geometry import parse_scene
+from mathutils import Vector, Matrix
+import trimesh
+
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core.util import blender as butil
+
+logger = logging.getLogger(__name__)
+
+@dataclass
+class Move:
+
+ names: typing.List[str]
+
+ def __post_init__(self):
+ assert isinstance(self.names, list)
+
+ def apply(self, state: state_def.State):
+ raise NotImplementedError
+
+ def revert(self, state: state_def.State):
+ raise NotImplementedError
+
+ def accept(self, state: state_def.State):
+ pass
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/pose.py b/infinigen/core/constraints/example_solver/moves/pose.py
new file mode 100644
index 000000000..454353538
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/pose.py
@@ -0,0 +1,122 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick, Karhan Kayan
+
+from dataclasses import dataclass
+import numpy as np
+import typing
+import logging
+
+import bpy
+from infinigen.core.constraints.example_solver.geometry import dof
+import mathutils
+
+from infinigen.core.util import blender as butil
+from infinigen.core.constraints.example_solver.geometry import validity, dof
+from infinigen.core.constraints.constraint_language import util as iu
+
+from . import moves
+from ..state_def import State
+from .reassignment import pose_backup, restore_pose_backup
+
+logger = logging.getLogger(__name__)
+
+@dataclass
+class TranslateMove(moves.Move):
+ # translate obj by vector
+
+ translation: np.array
+
+ _backup_pose: dict = None
+
+ def __repr__(self):
+ norm = np.linalg.norm(self.translation)
+ return f"{self.__class__.__name__}({self.names}, {norm:.2e})"
+
+ def apply(self, state: State):
+
+ target_name, = self.names
+
+ os = state.objs[target_name]
+ self._backup_pose = pose_backup(os, dof=False)
+
+ iu.translate(state.trimesh_scene, os.obj.name, self.translation)
+
+ if not validity.check_post_move_validity(state, target_name):
+ return False
+
+ return True
+
+ def revert(self, state: State):
+ target_name, = self.names
+ restore_pose_backup(state, target_name, self._backup_pose)
+
+@dataclass
+class RotateMove(moves.Move):
+
+ axis: np.array
+ angle: float
+
+ _backup_pose = None
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self.names}, {self.angle:.2e})"
+
+ def apply(self, state: State):
+
+ target_name, = self.names
+
+ os = state.objs[target_name]
+ self._backup_pose = pose_backup(os, dof=False)
+
+ iu.rotate(state.trimesh_scene, os.obj.name, self.axis, self.angle)
+
+ if not validity.check_post_move_validity(state, target_name):
+ return False
+
+ return True
+
+ def revert(self, state: State):
+ target_name, = self.names
+ restore_pose_backup(state, target_name, self._backup_pose)
+
+@dataclass
+class ReinitPoseMove(moves.Move):
+
+ _backup_pose: dict = None
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self.names})"
+
+ def apply(self, state: State):
+ target_name, = self.names
+ ostate = state.objs[target_name]
+ self._backup_pose = pose_backup(ostate)
+ return dof.try_apply_relation_constraints(state, target_name)
+
+ def revert(self, state: State):
+ target_name, = self.names
+ restore_pose_backup(state, target_name, self._backup_pose)
+
+'''
+@dataclass
+class ScaleMove(Move):
+ name: str
+ scale: np.array
+
+ def apply(self, state: State):
+ blender_obj = self.obj.bpy_obj
+ trimesh_obj = state.get_trimesh_object(self.obj.name)
+ blender_obj.scale *= Vector(self.scale)
+ trimesh_obj.apply_transform(trimesh.transformations.compose_matrix(scale=list(self.scale)))
+ self.obj.update()
+
+ def revert(self, state: State):
+ blender_obj = self.obj.bpy_obj
+ trimesh_obj = state.get_trimesh_object(self.obj.name)
+ blender_obj.scale /= Vector(self.scale)
+ trimesh_obj.apply_transform(trimesh.transformations.compose_matrix(scale=list(1/self.scale)))
+ self.obj.update()
+'''
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/reassignment.py b/infinigen/core/constraints/example_solver/moves/reassignment.py
new file mode 100644
index 000000000..421391db9
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/reassignment.py
@@ -0,0 +1,110 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Alexander Raistrick: primary author
+# - Karhan Kayan: sync with trimesh fix
+
+from dataclasses import dataclass
+import numpy as np
+import typing
+import logging
+import copy
+
+from infinigen.core.util import blender as butil
+from infinigen.core.constraints.constraint_language.util import (
+ translate,
+ rotate,
+ sync_trimesh
+)
+
+from infinigen.core.constraints.example_solver.geometry import validity
+from infinigen.core.constraints.example_solver.geometry import dof
+from . import moves
+from ..state_def import State, ObjectState
+
+def pose_backup(os: ObjectState, dof=True):
+
+ bak = dict(
+ loc=tuple(os.obj.location),
+ rot=tuple(os.obj.rotation_euler),
+ )
+
+ if dof:
+ bak['dof_trans'] = copy.copy(os.dof_matrix_translation)
+ bak['dof_rot'] = copy.copy(os.dof_rotation_axis)
+
+ return bak
+
+def restore_pose_backup(state, name, bak):
+ os = state.objs[name]
+ os.obj.location = bak['loc']
+ os.obj.rotation_euler = bak['rot']
+
+ if 'dof_trans' in bak:
+ os.dof_matrix_translation = bak['dof_trans']
+ if 'dof_rot' in bak:
+ os.dof_rotation_axis = bak['dof_rot']
+
+ sync_trimesh(state.trimesh_scene, state.objs[name].obj.name)
+
+@dataclass
+class RelationPlaneChange(moves.Move):
+
+ relation_idx: int
+ plane_idx: int
+
+ _backup_idx = None
+ _backup_poseinfo = None
+
+ def apply(self, state: State):
+
+ target_name, = self.names
+
+ os = state.objs[target_name]
+ rels = os.relations[self.relation_idx]
+
+ self._backup_idx = rels.parent_plane_idx
+ self._backup_poseinfo = pose_backup(os)
+
+ rels.parent_plane_idx = self.plane_idx
+
+ success = dof.try_apply_relation_constraints(state, target_name)
+ return success
+
+ def revert(self, state: State):
+
+ target_name, = self.names
+
+ os = state.objs[target_name]
+ os.relations[self.relation_idx].parent_plane_idx = self._backup_idx
+ restore_pose_backup(state, target_name, self._backup_poseinfo)
+
+@dataclass
+class RelationTargetChange(moves.Move):
+
+ # reassign obj to new parent
+ name: str
+ relation_idx: int
+ new_target: str
+
+ _backup_target = None
+ _backup_poseinfo = None
+
+ def apply(self, state: State):
+ os = state.objs[self.name]
+ rels = os.relations[self.relation_idx]
+
+ self._backup_target = rels.target_name
+ self._backup_poseinfo = pose_backup(os)
+ rels.target_name = self.new_target
+
+ return dof.try_apply_relation_constraints(state, self._new_name)
+
+ def revert(self, state: State):
+ os = state.objs[self.name]
+ rels = os.relations[self.relation_idx]
+
+ rels.target_name = self._backup_target
+ restore_pose_backup(state, self.name, self._backup_poseinfo)
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/moves/swap.py b/infinigen/core/constraints/example_solver/moves/swap.py
new file mode 100644
index 000000000..73b4ba846
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/moves/swap.py
@@ -0,0 +1,62 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from dataclasses import dataclass
+import numpy as np
+import typing
+import logging
+
+import bpy
+from infinigen.core.constraints.example_solver.geometry import parse_scene
+from mathutils import Vector, Matrix
+import trimesh
+
+from infinigen.core.constraints.example_solver import state_def
+from infinigen.core.util import blender as butil
+
+from infinigen.core.constraints.example_solver.moves import Move
+
+from .reassignment import pose_backup, restore_pose_backup
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class Swap(Move):
+ # swap the poses and relations of two objects
+
+ _obj1_backup = None
+ _obj2_backup = None
+
+ def __post_init__(self):
+ raise NotImplementedError(f"{self.__class__.__name__} untested")
+
+ def apply(self, state: state_def.State):
+
+ target1, target2 = self.names
+
+ o1 = state[target1].obj
+ o2 = state[target2].obj
+
+ self._obj1_backup = pose_backup(o1, dof=False)
+ self._obj2_backup = pose_backup(o2, dof=False)
+
+ o1.loc, o2.loc = o2.loc, o1.loc
+ o1.rotation_axis_angle, o2.rotation_axis_angle = o2.rotation_axis_angle, o1.rotation_axis_angle
+ o1.relation_assignments, o2.relation_assignments = o2.relation_assignments, o1.relation_assignments
+
+ def revert(self, state: state_def.State):
+
+ target1, target2 = self.names
+ restore_pose_backup(state, target1, self._obj1_backup)
+ restore_pose_backup(state, target2, self._obj2_backup)
+
+ o1 = state[target1].obj
+ o2 = state[target2].obj
+
+ o1.relation_assignments, o2.relation_assignments = o2.relation_assignments, o1.relation_assignments
+
+
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/populate.py b/infinigen/core/constraints/example_solver/populate.py
new file mode 100644
index 000000000..affbc6d9d
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/populate.py
@@ -0,0 +1,124 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors:
+# - Alexander Raistrick: populate_state_placeholders, apply_cutter
+# - Stamatis Alexandropoulos: Initial version of window cutting
+
+import logging
+
+import bpy
+from tqdm import tqdm
+
+from infinigen.core.constraints.example_solver.geometry import parse_scene
+
+from infinigen.core.constraints.example_solver.state_def import State
+from infinigen.core.constraints.constraint_language.util import delete_obj
+
+from infinigen.core.placement.placement import parse_asset_name
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints import usage_lookup
+
+logger = logging.getLogger(__name__)
+
+def apply_cutter(state, objkey, cutter):
+
+ os = state.objs[objkey]
+
+ cut_objs = []
+ for i, relation_state in enumerate(os.relations):
+
+ # TODO in theory we maybe should check if they actually intersect
+
+ parent_obj = state.objs[relation_state.target_name].obj
+ butil.modify_mesh(
+ parent_obj,
+ 'BOOLEAN',
+ object=butil.copy(cutter),
+ operation='DIFFERENCE',
+ solver='FAST'
+ )
+
+ target_obj_name = state.objs[relation_state.target_name].obj.name
+ cut_objs.append((relation_state.target_name, target_obj_name))
+
+ cutter_col = butil.get_collection('placeholders:asset_cutters')
+ butil.put_in_collection(cutter, cutter_col)
+
+ return cut_objs
+
+def populate_state_placeholders(state: State, filter=None, final=True):
+
+ logger.info(f'Populating placeholders {final=} {filter=}')
+ unique_assets = butil.get_collection('unique_assets')
+
+ if final:
+ for os in state.objs.values():
+ if t.Semantics.Room in os.tags:
+ os.obj = bpy.data.objects[os.obj.name + '.meshed']
+
+ targets = []
+
+ for objkey, os in state.objs.items():
+
+ if os.generator is None:
+ continue
+
+ if (
+ filter is not None
+ and not usage_lookup.has_usage(os.generator.__class__, filter)
+ ):
+ continue
+
+ if 'spawn_asset' in os.obj.name:
+ butil.put_in_collection(os.obj, unique_assets)
+ logger.debug(f'Found already populated asset {os.obj.name=}, continuing')
+ continue
+
+ targets.append(objkey)
+
+ update_state_mesh_objs = []
+
+ for i, objkey in enumerate(targets):
+
+ os = state.objs[objkey]
+ placeholder = os.obj
+
+ logger.info(f'Populating {i}/{len(targets)} {placeholder.name=}')
+
+ old_objname = placeholder.name
+ update_state_mesh_objs.append((objkey, old_objname))
+
+ *_, inst_seed = parse_asset_name(placeholder.name)
+ os.obj = os.generator.spawn_asset(
+ i=int(inst_seed),
+ loc=placeholder.location, # we could use placeholder=pholder here, but I worry pholder may have been modified
+ rot=placeholder.rotation_euler
+ )
+ os.generator.finalize_assets([os.obj])
+ butil.put_in_collection(os.obj, unique_assets)
+
+ cutter = next((o for o in butil.iter_object_tree(os.obj) if o.name.endswith('.cutter')), None)
+ logger.debug(f'{populate_state_placeholders.__name__} found {cutter=} for {os.obj.name=}')
+ if cutter is not None:
+ cut_objs = apply_cutter(state, objkey, cutter)
+ logger.debug(f'{populate_state_placeholders.__name__} cut {cutter.name=} from {cut_objs=}')
+ update_state_mesh_objs += cut_objs
+
+ if final:
+ return
+
+ # objects modified in any way (via pholder update or boolean cut) must be synched with trimesh state
+ for objkey, old_objname in tqdm(set(update_state_mesh_objs), desc='Updating trimesh with populated objects'):
+
+ os = state.objs[objkey]
+
+ # delete old trimesh
+ delete_obj(state.trimesh_scene, old_objname, delete_blender=False)
+
+ # put the new, populated object into the state
+ parse_scene.preprocess_obj(os.obj)
+ if not final:
+ tagging.tag_canonical_surfaces(os.obj)
+ parse_scene.add_to_scene(state.trimesh_scene, os.obj, preprocess=True)
diff --git a/infinigen/core/constraints/example_solver/propose_continous.py b/infinigen/core/constraints/example_solver/propose_continous.py
new file mode 100644
index 000000000..1e3aa9b56
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/propose_continous.py
@@ -0,0 +1,148 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import typing
+import logging
+
+import numpy as np
+import gin
+
+from .geometry import dof
+from mathutils import Vector
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ usage_lookup
+)
+
+from infinigen.core.constraints.evaluator.domain_contains import domain_contains
+
+from . import (
+ moves,
+ state_def
+)
+
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+TRANS_MULT = 8
+TRANS_MIN = 0.01
+ROT_MULT = np.pi
+ROT_MIN = 0 #2 * np.pi / 200
+
+ANGLE_STEP_SIZE = (2 * np.pi) / 8
+
+def get_pose_candidates(
+ consgraph: cl.Node,
+ state: state_def.State,
+ filter_domain: r.Domain,
+ require_rot_free: bool = False,
+):
+
+ return [
+ k for k, o in state.objs.items()
+ if o.active
+ and domain_contains(filter_domain, state, o)
+ and not (require_rot_free and o.dof_rotation_axis is None)
+ ]
+
+def propose_translate(
+ consgraph: cl.Node,
+ state: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float
+) -> typing.Iterator[moves.TranslateMove]:
+
+ candidates = get_pose_candidates(consgraph, state, filter_domain)
+ candidates = [c for c in candidates if state.objs[c].dof_matrix_translation is not None]
+ if not len(candidates):
+ return
+
+ while True:
+
+ obj_state_name = np.random.choice(candidates)
+ obj_state = state.objs[obj_state_name]
+
+ var = max(TRANS_MIN, TRANS_MULT * temperature)
+ random_vector = np.random.normal(0, var, size=3)
+ projected_vector = obj_state.dof_matrix_translation @ random_vector
+
+ yield moves.TranslateMove(
+ names=[obj_state_name],
+ translation=projected_vector,
+ )
+
+def propose_rotate(
+ consgraph: cl.Node,
+ state: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float
+) -> typing.Iterator[moves.RotateMove]:
+
+ candidates = get_pose_candidates(consgraph, state, filter_domain)
+ candidates = [
+ c for c in candidates if (
+ t.Semantics.NoRotation not in state.objs[c].tags
+ and state.objs[c].dof_rotation_axis is not None
+ and state.objs[c].dof_rotation_axis.dot(np.array((0,0,1))) > 0.95
+ )
+ ]
+ if not len(candidates):
+ return
+
+ while True:
+ obj_state_name = np.random.choice(candidates)
+ obj_state = state.objs[obj_state_name]
+
+ var = max(ROT_MIN, ROT_MULT * temperature)
+ random_angle = np.random.normal(0, var)
+
+ ang = random_angle / ANGLE_STEP_SIZE
+ ang = np.ceil(ang) if ang > 0 else np.floor(ang)
+ random_angle = ang * ANGLE_STEP_SIZE
+
+ axis = obj_state.dof_rotation_axis
+ yield moves.RotateMove(
+ names=[obj_state_name],
+ axis=axis,
+ angle=random_angle
+ )
+
+def propose_reinit_pose(
+ consgraph: cl.Node,
+ state: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float
+) -> typing.Iterator[moves.ReinitPoseMove]:
+
+ candidates = get_pose_candidates(consgraph, state, filter_domain)
+ candidates = [c for c in candidates if state.objs[c].dof_matrix_translation is not None]
+
+ if len(candidates) == 0:
+ return
+
+ while True:
+ obj_state_name = np.random.choice(candidates)
+ obj_state = state.objs[obj_state_name]
+
+ yield moves.ReinitPoseMove(
+ names=[obj_state_name],
+ )
+
+def propose_scale(
+ consgraph,
+ state,
+ temperature
+):
+ raise NotImplementedError
+ obj_state = np.random.choice(state.objs)
+ random_scale = np.random.normal(0, temperature, size=3)
+ return moves.ScaleMove(
+ name=obj_state.name,
+ scale=random_scale
+ )
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/propose_discrete.py b/infinigen/core/constraints/example_solver/propose_discrete.py
new file mode 100644
index 000000000..8b3b6d8f2
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/propose_discrete.py
@@ -0,0 +1,350 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Alexander Raistrick: primary author
+# - Karhan Kayan: fix bug to ensure deterministic behavior
+
+import logging
+import pdb
+import copy
+from itertools import product
+import typing
+
+import gin
+import numpy as np
+from pprint import pprint, pformat
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ usage_lookup
+)
+from infinigen.core.constraints.evaluator.domain_contains import (
+ domain_contains, objkeys_in_dom
+)
+from .geometry import planes
+from . import (
+ moves,
+ state_def,
+ propose_relations
+)
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util import blender as butil
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+class DummyCubeGenerator(AssetFactory):
+
+ def __init__(self, seed):
+ super().__init__(seed)
+
+ def create_asset(self, *_, **__):
+ return butil.spawn_cube()
+
+def lookup_generator(preds: set[t.Semantics]):
+
+ if t.contradiction(preds):
+ raise ValueError(f'Got lookup_generator for unsatisfiable {preds=}')
+
+ preds_pos, preds_neg = t.decompose_tags(preds)
+
+ fac_class_tags = [x.generator for x in preds if isinstance(x, t.FromGenerator)]
+ if len(fac_class_tags) > 1:
+ raise ValueError(f'{preds=} had {len(fac_class_tags)=}, only 1 is allowed')
+ elif len(fac_class_tags) == 1:
+ fac_class_tag, = fac_class_tags
+ usage = usage_lookup.usages_of_factory(fac_class_tag)
+ remainder = preds_pos - usage - {fac_class_tag}
+ if len(remainder):
+ raise ValueError(
+ f"Your constraint program requested {fac_class_tag} with {preds_pos}, "
+ f"but used_as[] tags specify {usage} so predicates {remainder=} are unsatisfied. "
+ f"Please add these remainder predicates to used_as for {fac_class_tag} so that it can be safely retrieved."
+ )
+ return [fac_class_tag]
+
+ options = usage_lookup.all_factories()
+ for pos_tag in preds_pos:
+ options &= usage_lookup.factories_for_usage(pos_tag)
+ for neg_tag in preds_neg:
+ options -= usage_lookup.factories_for_usage(neg_tag)
+
+ options = list(options)
+ # sort options to ensure deterministic behavior
+ options.sort(key=lambda x: x.__name__)
+ np.random.shuffle(options)
+
+ return options
+
+def propose_addition_bound_gen(
+ cons: cl.Node,
+ curr: state_def.State,
+ bounds: list[r.Bound],
+ goal_bound_idx: r.Bound,
+ gen_class: AssetFactory,
+ filter_domain: r.Domain
+):
+
+ '''
+ Try to propose any addition move involving the specified bound and generator
+ '''
+
+ goal_bound = bounds[goal_bound_idx]
+ logger.debug(f'attempt propose_addition for {gen_class.__name__} rels={len(goal_bound.domain.relations)}')
+
+ assert r.domain_finalized(goal_bound.domain), goal_bound
+ if not active_for_stage(goal_bound.domain, filter_domain):
+ raise ValueError(f'Attempted to propose {goal_bound} but it should not be active for {filter_domain=}')
+ if len(goal_bound.domain.relations) == 0:
+ raise ValueError(f'Attempted to propose unconstrained {gen_class.__name__} with no relations')
+
+ found_tags = usage_lookup.usages_of_factory(gen_class)
+ goal_pos, *_ = t.decompose_tags(goal_bound.domain.tags)
+ if not t.implies(found_tags, goal_pos) and found_tags.issuperset(goal_pos):
+ raise ValueError(f'Got {gen_class=} for {goal_pos=}, but it had {found_tags=}')
+
+ prop_dom = goal_bound.domain.intersection(filter_domain)
+ prop_dom.tags.update(found_tags)
+
+ #logger.debug(f'GOAL {goal_bound.domain} \nFILTER {filter_domain}\nPROP {prop_dom}\n\n')
+ logger.debug(
+ 'GOAL %s\n FILTER %s\n PROP %s\n',
+ goal_bound.domain.repr(abbrv=True),
+ filter_domain.repr(abbrv=True),
+ prop_dom,
+ )
+
+ assert active_for_stage(prop_dom, filter_domain)
+
+ search_rels = [
+ rd for rd in prop_dom.relations
+ if not isinstance(rd[0], cl.NegatedRelation)
+ ]
+
+ i = None
+ for i, assignments in enumerate(propose_relations.find_assignments(curr, search_rels)):
+
+ logger.debug(f'Found assignments %d %s %s', i, len(assignments), assignments)
+
+ yield moves.Addition(
+ names=[f'{np.random.randint(1e6):04d}_{gen_class.__name__}'], # decided later
+ gen_class=gen_class,
+ relation_assignments=assignments,
+ temp_force_tags=prop_dom.tags,
+ )
+
+ if i is None:
+ #raise ValueError(f'Found no assignments for {prop_dom}')
+ logger.debug(f'Found no assignments for {prop_dom.repr(abbrv=True)}')
+ pass
+ else:
+ logger.debug(f'Exhausted all assignments for {gen_class=}')
+
+def active_for_stage(
+ prop_dom: r.Domain,
+ filter_dom: r.Domain
+):
+ return prop_dom.intersects(filter_dom, require_satisfies_right=True)
+
+@gin.configurable
+def preproc_bounds(
+ bounds: list[r.Bound],
+ state: state_def.State,
+ filter: r.Domain,
+ reverse=False,
+ shuffle=True,
+ print_bounds=False
+):
+
+ if print_bounds:
+ print(f"{preproc_bounds.__name__} for {filter.get_objs_named()} (total {len(bounds)}):")
+ for b in bounds:
+ res = active_for_stage(b.domain, filter)
+ if res:
+ print("BOUND", res, b.domain.intersection(filter).repr(abbrv=True), "\n")
+
+ for b in bounds:
+ if not r.domain_finalized(b.domain, check_anyrel=False, check_variable=True):
+ raise ValueError(f'{preproc_bounds.__name__} found non-finalized {b.domain=}')
+
+ bounds = [
+ b for b in bounds if active_for_stage(b.domain, filter)
+ ]
+
+ if shuffle:
+ np.random.shuffle(bounds)
+
+ bound_counts = [
+ len(objkeys_in_dom(b.domain, state))
+ for b in bounds
+ ]
+
+ order = np.arange(len(bounds))
+
+ def key(i):
+ b = bounds[i]
+ bc = bound_counts[i]
+ if b.high is not None and b.high < bc:
+ res = 1
+ elif b.low is not None and b.low > bc:
+ res = -1
+ else:
+ res = 0
+ return -res if reverse else res
+
+ order = sorted(order, key=key)
+
+ return [bounds[i] for i in order if key(i) != 1]
+
+def propose_addition(
+ cons: cl.Node,
+ curr: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float,
+):
+
+ bounds = r.constraint_bounds(cons)
+ bounds = preproc_bounds(bounds, curr, filter_domain)
+
+ if len(bounds) == 0:
+ logger.debug(f'Found no bounds for {filter_domain=}')
+ return
+
+ for i, bound in enumerate(bounds):
+
+ if bound.low is None:
+ # bounds with low=None are supposed to cap other bounds, not introduce new objects
+ continue
+
+ fac_options = lookup_generator(preds=bound.domain.tags)
+ if len(fac_options) == 0:
+ if bound.low is None or bound.low == 0:
+ continue
+ raise ValueError(f'Found no generators for {bound}')
+
+ for gen_class in fac_options:
+ yield from propose_addition_bound_gen(cons, curr, bounds, i, gen_class, filter_domain)
+
+ logger.debug(f'propose_addition found no candidate moves for {bound}')
+
+def propose_deletion(
+ cons: cl.Node,
+ curr: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float,
+):
+
+ bounds = r.constraint_bounds(cons)
+ bounds = preproc_bounds(bounds, curr, filter_domain, reverse=True, shuffle=True)
+
+ if len(bounds) == 0:
+ logger.debug(f'Found no bounds for {filter_domain=}')
+ return
+
+ np.random.shuffle(bounds)
+
+ for i, bound in enumerate(bounds):
+ candidates = objkeys_in_dom(bound.domain, curr)
+ np.random.shuffle(candidates)
+ for cand in candidates:
+ yield moves.Deletion([cand])
+
+def propose_relation_plane_change(
+ cons: cl.Node,
+ curr: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float,
+):
+ cand_objs = objkeys_in_dom(filter_domain, curr)
+
+ if len(cand_objs) == 0:
+ logger.debug(f'Found no cand_objs for {filter_domain=}')
+ return
+
+ np.random.shuffle(cand_objs)
+ for cand in cand_objs:
+ for i, rels in enumerate(curr.objs[cand].relations):
+
+ if not isinstance(rels.relation, cl.GeometryRelation):
+ continue
+
+ target_obj = curr.objs[rels.target_name].obj
+ n_planes = len(curr.planes.get_tagged_planes(target_obj, rels.relation.parent_tags))
+ if n_planes <= 1:
+ continue
+
+ order = np.arange(n_planes)
+ np.random.shuffle(order)
+ for plane_idx in order:
+ if plane_idx == rels.parent_plane_idx:
+ continue
+ yield moves.RelationPlaneChange(
+ names=[cand],
+ relation_idx=i,
+ plane_idx=plane_idx
+ )
+
+def propose_resample(
+ cons: cl.Node,
+ curr: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float,
+):
+
+ cand_objs = objkeys_in_dom(filter_domain, curr)
+
+ if len(cand_objs) == 0:
+ logger.debug(f'Found no cand_objs for {filter_domain=}')
+ return
+
+ np.random.shuffle(cand_objs)
+
+ for cand in cand_objs:
+
+ os = curr.objs[cand]
+ if usage_lookup.has_usage(os.generator.__class__, t.Semantics.SingleGenerator):
+ continue
+
+ yield moves.Resample(names=[cand], align_corner=None)
+
+ #corner_options = [None] + list(range(6))
+ #np.random.shuffle(corner_options)
+ #for c in corner_options:
+ # yield moves.Resample(name=cand, align_corner=c)
+
+def is_swap_domains_unaffected(
+ state: state_def.State,
+ name1: str,
+ name2: str
+):
+ raise NotImplementedError()
+
+def propose_swap(
+ cons: cl.Node,
+ curr: state_def.State,
+ filter_domain: r.Domain,
+ temperature: float,
+):
+
+ raise NotImplementedError()
+
+ cand_objs = objkeys_in_dom(filter_domain, curr)
+
+ if len(cand_objs) == 0:
+ logger.debug(f'Found no cand_objs for {filter_domain=}')
+ return
+
+ a_objs = copy.copy(cand_objs)
+ b_objs = copy.copy(cand_objs)
+ np.random.shuffle(a_objs)
+ np.random.shuffle(b_objs)
+
+ for a, b in product(a_objs, b_objs):
+ if a == b:
+ continue
+ if not is_swap_domains_unaffected(curr, a, b):
+ continue
+ yield moves.Swap(names=[a, b])
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/propose_relations.py b/infinigen/core/constraints/example_solver/propose_relations.py
new file mode 100644
index 000000000..841d873b8
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/propose_relations.py
@@ -0,0 +1,151 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import logging
+import typing
+
+import numpy as np
+from pprint import pprint
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+)
+from infinigen.core.constraints.evaluator.domain_contains import (
+ objkeys_in_dom
+)
+from .geometry import planes
+from . import (
+ moves,
+ state_def
+)
+
+from infinigen.core import tags as t, tagging
+
+logger = logging.getLogger(__name__)
+
+def minimize_redundant_relations(
+ relations: list[tuple[cl.Relation, r.Domain]]
+):
+
+ """
+ Given a list of relations that must be true, use the first as a constraint to tighten the remaining relations
+ """
+
+ assert len(relations) > 0
+
+ # TODO Hacky: moves AnyRelations to the back so _hopefully_ they get implied before we get to them
+ relations = sorted(
+ relations,
+ key=lambda r: isinstance(r[0], cl.AnyRelation),
+ reverse=True
+ )
+
+ (rel, dom), *rest = relations
+
+ # Force all remaining relations to be compatible with (rel, dom), thereby reducing their search space
+ remaining_relations = []
+ for (r_later, d_later) in rest:
+
+ logger.debug(f'Inspecting {r_later=} {d_later=}')
+
+ if d_later.intersects(dom):
+ logger.debug(f"Intersecting {d_later} with {dom}")
+ d_later = d_later.intersection(dom)
+
+ if r.reldom_implies((rel, dom), (r_later, d_later)):
+ # (rlater, dlater) is guaranteed true so long as we satisfied (rel, dom), we dont need to separately assign it
+ logger.debug(f'Discarding since rlater,dlater it is implied')
+ continue
+ else:
+ logger.debug(f'Keeping {r_later, d_later} since it is not implied by {rel, dom} ')
+ remaining_relations.append((r_later, d_later))
+
+ implied = any(
+ r.reldom_implies(reldom_later, (rel, dom))
+ for reldom_later in remaining_relations
+ )
+
+ return (rel, dom), remaining_relations, implied
+
+
+def find_assignments(
+ curr: state_def.State,
+ relations: list[tuple[cl.Relation, r.Domain]],
+ assignments: list[state_def.RelationState] = None,
+) -> typing.Iterator[list[state_def.RelationState]]:
+
+ """Iterate over possible assignments that satisfy the given relations. Some assignments may not be feasible geometrically -
+ a naive implementation of this function would just enumerate all possible objects matching the assignments, and let the solver
+ discover that many combinations are impossible. *This* implementation attemps to never generate guaranteed-invalid combinations in the first place.
+
+ Complexity is pretty astronomical:
+ - N^M where N is number of candidates per relation, and M is number of relations
+ - reduced somewhat when relations intersect or imply eachother
+ - luckily, M is typically 1, 2 or 3, as objects arent often related to lots of other objects
+
+ TODO:
+ - discover new relations constraints, which can arise from the particular choice of objects
+ - prune early when object choice causes bounds to be violated
+
+ This function essentially does a complex form of SAT-solving. It *really* shouldnt be written in python
+ """
+
+ if assignments is None:
+ assignments = []
+ #print('FIND ASSIGNMENTS TOPLEVEL')
+ #pprint(relations)
+
+ if len(relations) == 0:
+ yield assignments
+ return
+
+ logger.debug(f'Attempting to assign {relations[0]}')
+
+ (rel, dom), remaining_relations, implied = minimize_redundant_relations(relations)
+ assert len(remaining_relations) < len(relations)
+
+ if implied:
+ logger.debug(f'Found remaining_relations implies {(rel, dom)=}, skipping it')
+ yield from find_assignments(
+ curr,
+ relations=remaining_relations,
+ assignments=assignments
+ )
+ return
+
+ if isinstance(rel, cl.AnyRelation):
+ pprint(relations)
+ pprint([(rel, dom)] + remaining_relations)
+ raise ValueError(f'Got {rel} as first relation. Invalid! Maybe the program is underspecified?')
+
+ candidates = objkeys_in_dom(dom, curr)
+
+ for parent_candidate_name in candidates:
+ logging.debug(f'{parent_candidate_name=}')
+
+ parent_state = curr.objs[parent_candidate_name]
+ n_parent_planes = len(curr.planes.get_tagged_planes(parent_state.obj, rel.parent_tags))
+
+ parent_order = np.arange(n_parent_planes)
+ np.random.shuffle(parent_order)
+
+ for parent_plane in parent_order:
+
+ #logger.debug(f'Considering {parent_candidate_name=} {parent_plane=} {n_parent_planes=}')
+
+ assignment = state_def.RelationState(
+ relation=rel,
+ target_name=parent_candidate_name,
+ child_plane_idx=0, # TODO fill in at apply()-time
+ parent_plane_idx=parent_plane,
+ )
+
+ yield from find_assignments(
+ curr,
+ relations=remaining_relations,
+ assignments=assignments + [assignment],
+ )
diff --git a/infinigen/core/constraints/example_solver/room/__init__.py b/infinigen/core/constraints/example_solver/room/__init__.py
new file mode 100644
index 000000000..58157b2f5
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from .blueprint import RoomSolver, MultistoryRoomSolver
+from .graph import GraphMaker
diff --git a/infinigen/core/constraints/example_solver/room/blueprint.py b/infinigen/core/constraints/example_solver/room/blueprint.py
new file mode 100644
index 000000000..e2e3dddc8
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/blueprint.py
@@ -0,0 +1,218 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Lingjie Mei, Karhan Kayan: fix constants
+
+from copy import deepcopy
+
+import bpy
+import numpy as np
+from numpy.random import uniform
+from shapely import Polygon
+from tqdm import tqdm, trange
+import gin
+
+from infinigen.assets.utils.misc import toggle_hide
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import random_general as rg
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+from .constants import WALL_HEIGHT
+
+from .graph import GraphMaker
+from .scorer import BlueprintScorer, JointBlueprintScorer
+from .contour import ContourFactory
+from .solidifier import BlueprintSolidifier
+from .segment import SegmentMaker
+from .solver import BlueprintSolver, BlueprintStaircaseSolver
+from .utils import polygon2obj, unit_cast
+from infinigen.core.constraints.example_solver.room import constants
+
+from infinigen.core.constraints.example_solver.state_def import State, ObjectState
+from infinigen.core.constraints.constraint_language import Semantics
+
+
+@gin.configurable
+class RoomSolver:
+
+ def __init__(self, factory_seed, n_divide_trials=2500, iters_mult=150, ):
+ self.factory_seed = factory_seed
+ with FixedSeed(factory_seed):
+ self.graph_maker = GraphMaker(factory_seed)
+ self.graph = self.graph_maker.make_graph(np.random.randint(1e7))
+ self.width, self.height = self.graph_maker.suggest_dimensions(self.graph)
+ self.contour_factory = ContourFactory(self.width, self.height)
+ self.contour = self.contour_factory.make_contour(np.random.randint(1e7))
+
+ n = len(self.graph.neighbours)
+
+ self.segment_maker = SegmentMaker(self.factory_seed, self.contour, n)
+ self.solver = BlueprintSolver(self.contour, self.graph)
+ self.scorer = BlueprintScorer(self.graph)
+ self.solidifier = BlueprintSolidifier(self.graph, 0)
+
+ self.n_divide_trials = n_divide_trials
+ self.iterations = iters_mult * n
+ self.score_scale = 5
+
+ def simulated_anneal(self, assignment, info):
+ score = self.scorer.find_score(assignment, info)
+ with tqdm(total=self.iterations, desc='Sampling solutions') as pbar:
+ while pbar.n < self.iterations:
+ assignment_, info_ = deepcopy(assignment), deepcopy(info)
+ resp = self.solver.perturb_solution(assignment_, info_)
+ if not resp.is_success:
+ continue
+ pbar.update(1)
+ score_ = self.scorer.find_score(assignment_, info_)
+ scale = self.score_scale * pbar.n / self.iterations
+ if np.log(uniform()) < (score - score_) * scale:
+ assignment, info, score = assignment_, info_, score_
+ pbar.set_description(f'loss={score:.4f}')
+
+ return assignment, info
+
+ def solve(self):
+
+ assignment, info = [], {}
+ for i in range(self.n_divide_trials):
+ info = self.segment_maker.build_segments()
+ assignment = self.solver.find_assignment(info)
+ if assignment is not None:
+ break
+
+ if assignment is None:
+ raise ValueError(f'{self.__class__.__name__} got {assignment=} after {self.n_divide_trials=}')
+
+ assignment, info = self.simulated_anneal(assignment, info)
+
+ state, rooms_meshed = self.solidifier.solidify(assignment, info)
+
+ unique_roomtypes = set(Semantics(s.split('_')[0]) for s in self.graph.rooms)
+ dimensions = self.width, self.height, constants.WALL_HEIGHT
+
+ return state, unique_roomtypes, dimensions
+
+
+@gin.configurable
+class MultistoryRoomSolver:
+
+ def __init__(self, factory_seed, n_divide_trials=2500, iters_mult=150,
+ n_stories=('categorical', 0., .0, .5 ,.5), fixed_contour=('bool', .5)):
+ self.factory_seed = factory_seed
+ with FixedSeed(factory_seed):
+ self.n_stories = rg(n_stories)
+ self.fixed_contour = rg(fixed_contour)
+ self.n_contour_trials = 100
+ self.graph_makers, self.graphs = [], []
+ self.widths, self.heights = [], []
+ self.build_graphs(factory_seed)
+ self.contour_factories, self.contours = [], []
+ self.build_contours()
+
+ self.segment_makers = [SegmentMaker(self.factory_seed, self.contours[i], len(self.graphs[i])) for i
+ in range(self.n_stories)]
+ self.solvers = [BlueprintSolver(self.contours[i], self.graphs[i]) for i in range(self.n_stories)]
+ self.staircase_solver = BlueprintStaircaseSolver(self.contours)
+ self.scorer = JointBlueprintScorer(self.graphs)
+ self.solidifiers = [BlueprintSolidifier(self.graphs[i], i) for i in range(self.n_stories)]
+
+ self.n_divide_trials = n_divide_trials
+ self.iterations = iters_mult * sum(len(g) for g in self.graphs)
+ self.score_scale = 5
+ self.staircase_solver_prob = .1
+
+ def build_graphs(self, factory_seed):
+ for i in range(self.n_stories):
+ kwargs = {'entrance_type': 'none'} if i > 0 else {}
+ graph_maker = GraphMaker(factory_seed, i, self.n_stories > 1, **kwargs)
+ self.graph_makers.append(graph_maker)
+ if self.fixed_contour and i > 0:
+ width, height = self.widths[-1], self.heights[-1]
+ graph = graph_maker.make_graph(np.random.randint(1e6))
+ else:
+ for j in range(self.n_contour_trials):
+ graph = graph_maker.make_graph(np.random.randint(1e6))
+ args = [self.widths[-1], self.heights[-1]] if len(self.graphs) > 0 else [None, None]
+ width, height = graph_maker.suggest_dimensions(graph, *args)
+ if width is not None and height is not None:
+ break
+ else:
+ raise Exception('Invalid graph')
+ self.widths.append(width)
+ self.heights.append(height)
+ self.graphs.append(graph)
+
+ def build_contours(self):
+ for i in range(self.n_stories):
+ while len(self.contours) <= i:
+ for j in range(self.n_contour_trials):
+ contour_factory = ContourFactory(self.widths[i], self.heights[i])
+ if self.fixed_contour and i > 0:
+ contour = self.contours[-1]
+ else:
+ contour = contour_factory.make_contour(np.random.randint(1e6))
+ if len(self.contours) > 0:
+ x_offset = unit_cast((self.widths[i] - self.widths[0]) / 2)
+ y_offset = unit_cast((self.heights[i] - self.heights[0]) / 2)
+ contour = Polygon(
+ [(x - x_offset, y - y_offset) for x, y in contour.boundary.coords[:]])
+ if not self.contours[-1].contains(contour):
+ continue
+ self.contour_factories.append(contour_factory)
+ self.contours.append(contour)
+ break
+ else:
+ self.widths[i] -= constants.UNIT
+ self.heights[i] -= constants.UNIT
+
+ def solve(self):
+ assignments, infos = [], []
+ while len(assignments) == 0:
+ staircase = self.contour_factories[-1].add_staircase(self.contours[-1])
+ for j in range(self.n_stories):
+ for _ in trange(self.n_divide_trials, desc=f'Dividing segments for {j}'):
+ info = self.segment_makers[j].build_segments(staircase)
+ assignment = self.solvers[j].find_assignment(info)
+ if assignment is not None:
+ assignments.append(assignment)
+ infos.append(info)
+ break
+ else:
+ assignments, infos = [], []
+ break
+
+ assignments, infos = self.simulated_anneal(assignments, infos)
+
+ obj_states = {}
+ for j in range(self.n_stories):
+ state, rooms_meshed = self.solidifiers[j].solidify(assignments[j], infos[j])
+ obj_states.update(state.objs)
+ unique_roomtypes = set()
+ for graph in self.graphs:
+ for s in graph.rooms:
+ unique_roomtypes.add(Semantics(s.split('_')[0]))
+ dimensions = self.widths[0], self.heights[0], WALL_HEIGHT * self.n_stories
+ return State(obj_states), unique_roomtypes, dimensions
+
+ def simulated_anneal(self, assignments, infos):
+ score = self.scorer.find_score(assignments, infos)
+ with tqdm(total=self.iterations, desc='Sampling solutions') as pbar:
+ while pbar.n < self.iterations:
+ assignments_, infos_ = deepcopy(assignments), deepcopy(infos)
+ if uniform() < self.staircase_solver_prob:
+ resp = self.staircase_solver.perturb_solution(assignments, infos)
+ else:
+ probs = np.array([len(g) for g in self.graphs])
+ j = np.random.choice(np.arange(self.n_stories), p=probs / probs.sum())
+ resp = self.solvers[j].perturb_solution(assignments_[j], infos_[j])
+ if not resp.is_success:
+ continue
+ pbar.update(1)
+ score_ = self.scorer.find_score(assignments_, infos_)
+ scale = self.score_scale * pbar.n / self.iterations
+ if np.log(uniform()) < (score - score_) * scale:
+ assignments, infos, score = assignments_, infos_, score_
+ return assignments, infos
diff --git a/infinigen/core/constraints/example_solver/room/configs.py b/infinigen/core/constraints/example_solver/room/configs.py
new file mode 100644
index 000000000..8c481faac
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/configs.py
@@ -0,0 +1,132 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections import defaultdict
+
+from infinigen.assets.materials import brick, hardwood_floor, plaster, rug, tile
+from infinigen.assets.materials.woods import tiled_wood
+from infinigen.assets.materials.stone_and_concrete import concrete
+from infinigen.core.constraints.example_solver.room.types import RoomType
+from infinigen.core.util.color import hsv2rgba
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.random import random_general as rg
+
+EXTERIOR_CONNECTED_ROOM_TYPES = [RoomType.Bedroom, RoomType.Garage, RoomType.Balcony, RoomType.DiningRoom,
+ RoomType.Kitchen, RoomType.LivingRoom]
+SQUARE_ROOM_TYPES = [RoomType.Kitchen, RoomType.Bedroom, RoomType.LivingRoom, RoomType.Closet,
+ RoomType.Bathroom, RoomType.Garage, RoomType.Balcony, RoomType.DiningRoom, RoomType.Utility]
+TYPICAL_AREA_ROOM_TYPES = {
+ RoomType.Kitchen: 20,
+ RoomType.Bedroom: 25,
+ RoomType.LivingRoom: 30,
+ RoomType.DiningRoom: 20,
+ RoomType.Closet: 4,
+ RoomType.Bathroom: 8,
+ RoomType.Utility: 4,
+ RoomType.Garage: 30,
+ RoomType.Balcony: 8,
+ RoomType.Hallway: 8,
+ RoomType.Staircase: 20,
+}
+ROOM_NUMBERS = {RoomType.Bathroom: (1, 10), RoomType.LivingRoom: (1, 10)}
+COMBINED_ROOM_TYPES = [[RoomType.Hallway, RoomType.LivingRoom, RoomType.DiningRoom], [RoomType.Garage]]
+PANORAMIC_ROOM_TYPES = {
+ RoomType.Hallway: .3,
+ RoomType.LivingRoom: .5,
+ RoomType.DiningRoom: .5,
+ RoomType.Balcony: 1,
+}
+FUNCTIONAL_ROOM_TYPES = [RoomType.Kitchen, RoomType.Bedroom, RoomType.LivingRoom, RoomType.Bathroom,
+ RoomType.DiningRoom]
+WINDOW_ROOM_TYPES = defaultdict(lambda: 1, {
+ RoomType.Utility: .3,
+ RoomType.Closet: 0.,
+ RoomType.Bathroom: .5,
+ RoomType.Garage: .5,
+})
+
+
+def make_room_colors():
+ bedroom_color = hsv2rgba(0., .8, log_uniform(.02, .1))
+ hallway_color = hsv2rgba(.4, .8, log_uniform(.02, .1))
+ utility_color = hsv2rgba(.8, .8, log_uniform(.02, .1))
+ return {
+ RoomType.Kitchen: hallway_color,
+ RoomType.Bedroom: bedroom_color,
+ RoomType.LivingRoom: hallway_color,
+ RoomType.Closet: bedroom_color,
+ RoomType.Hallway: hallway_color,
+ RoomType.Bathroom: bedroom_color,
+ RoomType.Garage: utility_color,
+ RoomType.Balcony: utility_color,
+ RoomType.DiningRoom: hallway_color,
+ RoomType.Utility: utility_color,
+ RoomType.Staircase: hallway_color,
+ }
+
+
+ROOM_COLORS = make_room_colors()
+ROOM_CHILDREN = defaultdict(dict, {
+ RoomType.LivingRoom: {
+ RoomType.LivingRoom: ('bool', .1),
+ RoomType.Bedroom: ('categorical', .0, .45, .4, .1, .05),
+ RoomType.Closet: ('bool', .1),
+ RoomType.Bathroom: ('bool', .2),
+ RoomType.Garage: ('bool', .2),
+ RoomType.Balcony: ('bool', .2),
+ RoomType.DiningRoom: ('bool', 1.0),
+ RoomType.Utility: ('bool', .2),
+ RoomType.Hallway: ('categorical', .5, .4, .1)
+ },
+ RoomType.Kitchen: {RoomType.Garage: ('bool', .5), RoomType.Utility: ('bool', .1)
+ },
+ RoomType.Bedroom: {RoomType.Bathroom: ('bool', .3), RoomType.Closet: ('bool', .5)},
+ RoomType.Bathroom: {RoomType.Closet: ('bool', .2)},
+ RoomType.DiningRoom: {RoomType.Kitchen: ('bool', 1.), RoomType.Hallway: ('bool', .2)
+ }
+})
+
+STUDIO_ROOM_CHILDREN = defaultdict(dict, {
+ RoomType.LivingRoom: {
+ RoomType.Bedroom: ('categorical', .0, 1.),
+ RoomType.DiningRoom: ('bool', 1.),
+ },
+ RoomType.Bedroom: {RoomType.Bathroom: ('bool', 1.)},
+ RoomType.DiningRoom: {RoomType.Kitchen: ('bool', 1.)
+ }
+})
+UPSTAIRS_ROOM_CHILDREN = defaultdict(dict, {
+ RoomType.LivingRoom: {
+ RoomType.Bedroom: ('categorical', .0, .4, .5, .2),
+ RoomType.Closet: ('bool', .2),
+ RoomType.Bathroom: ('bool', .4),
+ RoomType.Balcony: ('bool', .4),
+ RoomType.Utility: ('bool', .2),
+ RoomType.Hallway: ('categorical', .0, .5, .5)
+ },
+ RoomType.Bedroom: {RoomType.Bathroom: ('bool', .3), RoomType.Closet: ('bool', .5)},
+ RoomType.Bathroom: {RoomType.Closet: ('bool', .2)},
+ RoomType.Balcony: {RoomType.Utility: ('bool', .4), RoomType.Hallway: ('bool', .1)},
+})
+LOOP_ROOM_TYPES = {
+ RoomType.LivingRoom: {RoomType.Garage: .2, RoomType.Balcony: .2, RoomType.Kitchen: .1},
+ RoomType.Bedroom: {RoomType.Balcony: .1},
+}
+
+ROOM_WALLS = defaultdict(lambda: plaster, {
+ RoomType.Kitchen: ('weighted_choice', (2, tile), (5, plaster)),
+ RoomType.Garage: ('weighted_choice', (5, concrete), (1, brick), (3, plaster)),
+ RoomType.Utility: ('weighted_choice', (1, concrete), (1, brick), (1, brick), (5, plaster)),
+ RoomType.Balcony: ('weighted_choice', (1, brick), (5, plaster)),
+ RoomType.Bathroom: tile
+})
+
+ROOM_FLOORS = defaultdict(lambda: ('weighted_choice', (3, tiled_wood), (1, tile), (1, rug)), {
+ RoomType.Garage: concrete,
+ RoomType.Utility: ('weighted_choice', (1, concrete), (1, plaster), (1, tile)),
+ RoomType.Bathroom: tile,
+ RoomType.Balcony: tile
+})
+
+PILLAR_ROOM_TYPES = [RoomType.Hallway, RoomType.LivingRoom, RoomType.Staircase, RoomType.DiningRoom]
diff --git a/infinigen/core/constraints/example_solver/room/constants.py b/infinigen/core/constraints/example_solver/room/constants.py
new file mode 100644
index 000000000..c8fec8bc3
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/constants.py
@@ -0,0 +1,100 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: bug fixes
+
+import gin
+import numpy as np
+
+from infinigen.core.util.random import random_general as rg
+
+
+def make_np(xs):
+ return [np.array(x) for x in xs]
+
+
+@gin.configurable
+def global_params(unit=.5, segment_margin=1.2, wall_thickness=('uniform', .2, .3),
+ wall_height=('uniform', 2.7, 3.8)):
+ wall_thickness = rg(wall_thickness)
+ wall_height = rg(wall_height)
+ return {
+ 'unit': unit,
+ 'segment_margin': segment_margin,
+ 'wall_thickness': wall_thickness,
+ 'wall_height': wall_height
+ }
+
+
+UNIT, SEGMENT_MARGIN, WALL_THICKNESS, WALL_HEIGHT = make_np(global_params().values())
+
+
+@gin.configurable
+def door_params(door_width=('uniform', .85, 1), door_size=('uniform', 2., 2.4)):
+ door_width = rg(door_width)
+ assert door_width > 0
+ door_margin = (door_width + WALL_THICKNESS) / 2
+ door_size = rg(door_size)
+ return {'door_width': door_width, 'door_margin': door_margin, 'door_size': door_size, }
+
+
+DOOR_WIDTH, DOOR_MARGIN, DOOR_SIZE = make_np(door_params().values())
+
+
+@gin.configurable
+def window_params(
+ max_window_length=('uniform', 6, 8),
+ window_height=('uniform', .4, 1.2),
+ window_margin=('uniform', .2, .6)
+):
+ max_window_length = rg(max_window_length)
+ window_height = rg(window_height)
+ window_margin = rg(window_margin)
+ window_size = WALL_HEIGHT - WALL_THICKNESS - window_height - window_margin
+ assert window_size > 0
+ return {
+ 'max_window_length': max_window_length,
+ 'window_height': window_height,
+ 'window_margin': window_margin,
+ 'window_size': window_size,
+ }
+
+
+MAX_WINDOW_LENGTH, WINDOW_HEIGHT, WINDOW_MARGIN, WINDOW_SIZE = make_np(window_params().values())
+
+
+@gin.configurable
+def staircase_params(staircase_snap=('uniform', .8, 1.2)):
+ return {'staircase_snap': rg(staircase_snap)}
+
+
+STAIRCASE_SNAP = make_np(staircase_params().values())
+
+
+def init_global_params():
+ ys = make_np(global_params().values())
+ xs = UNIT, SEGMENT_MARGIN, WALL_THICKNESS, WALL_HEIGHT
+ for x, y in zip(xs, ys):
+ x.fill(y)
+
+
+def init_door_params():
+ ys = make_np(door_params().values())
+ xs = DOOR_WIDTH, DOOR_MARGIN, DOOR_SIZE
+ for x, y in zip(xs, ys):
+ x.fill(y)
+
+
+def init_window_params():
+ ys = make_np(window_params().values())
+ xs = MAX_WINDOW_LENGTH, WINDOW_HEIGHT, WINDOW_MARGIN, WINDOW_SIZE
+ for x, y in zip(xs, ys):
+ x.fill(y)
+
+def initialize_constants():
+ init_global_params()
+ init_door_params()
+ init_window_params()
\ No newline at end of file
diff --git a/infinigen/core/constraints/example_solver/room/contour.py b/infinigen/core/constraints/example_solver/room/contour.py
new file mode 100644
index 000000000..9b116c1d2
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/contour.py
@@ -0,0 +1,132 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import random
+
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+from shapely import Polygon, box
+
+from infinigen.core.constraints.example_solver.room.utils import unit_cast
+from infinigen.core.constraints.example_solver.room.types import RoomType
+from infinigen.core.constraints.example_solver.room.configs import TYPICAL_AREA_ROOM_TYPES
+from infinigen.assets.utils.decorate import read_co, write_co
+from infinigen.assets.utils.object import new_plane
+from infinigen.core.util import blender as butil
+from infinigen.core.util.math import FixedSeed, int_hash
+from infinigen.core.util.random import log_uniform
+
+LARGE = 100
+
+
+@gin.configurable(denylist=['width', 'height'])
+class ContourFactory:
+ def __init__(self, width=17, height=9):
+ self.width = width
+ self.height = height
+ self.n_trials = 1000
+
+ def make_contour(self, i):
+ with FixedSeed(i):
+ obj = new_plane()
+ obj.location = self.width / 2, self.height / 2, 0
+ obj.scale = self.width / 2, self.height / 2, 1
+ butil.apply_transform(obj, loc=True)
+ corners = list((x, y) for x in [0, unit_cast(self.width)] for y in [0, unit_cast(self.height)])
+ random.shuffle(corners)
+ corners = dict(enumerate(corners))
+
+ def nearest(t):
+ if len(corners) == 0:
+ return -1, np.inf
+ c = np.array(list(corners.values()))
+ dist = np.abs(c - np.array([[t[0], t[1]]])).sum(1)
+ return list(corners.keys())[np.argmin(dist)], np.min(dist)
+
+ while len(corners) > 0:
+ _, (x, y) = corners.popitem()
+ r = uniform(0, 1)
+ if r < .2:
+ axes = []
+ if nearest((self.width - x, y))[1] < .1:
+ axes.append(0)
+ elif nearest((x, self.height - y))[1] < .1:
+ axes.append(1)
+ if len(axes) > 0:
+ axis = np.random.choice(axes)
+ self.add_long_corner(obj, x, y, axis)
+ t = (self.width - x, y) if axis == 0 else (x, self.height - y)
+ corners.pop(nearest(t)[0])
+ elif r < .35:
+ self.add_round_corner(obj, x, y)
+ elif r < .5:
+ self.add_straight_corner(obj, x, y)
+ elif r < .65:
+ self.add_sharp_corner(obj, x, y)
+
+ vertices = obj.data.polygons[0].vertices
+ p = Polygon(read_co(obj)[:, :2][vertices])
+ butil.delete(obj)
+ return p
+
+ def add_round_corner(self, obj, x, y):
+ vg = obj.vertex_groups.new(name='corner')
+ for i, v in enumerate(obj.data.vertices):
+ vg.add([i], v.co[0] == x and v.co[1] == y, 'REPLACE')
+ width = unit_cast(uniform(.2, .3) * min(self.width, self.height))
+ try:
+ butil.modify_mesh(obj, 'BEVEL', affect='VERTICES', limit_method='VGROUP', vertex_group='corner',
+ segments=np.random.randint(2, 5), width=width)
+ except:
+ pass
+ obj.vertex_groups.remove(obj.vertex_groups['corner'])
+
+ def add_straight_corner(self, obj, x, y):
+ vg = obj.vertex_groups.new(name='corner')
+ for i, v in enumerate(obj.data.vertices):
+ vg.add([i], v.co[0] == x and v.co[1] == y, 'REPLACE')
+ width = unit_cast(uniform(.1, .3) * min(self.width, self.height))
+ if width > 0:
+ butil.modify_mesh(obj, 'BEVEL', affect='VERTICES', limit_method='VGROUP', vertex_group='corner',
+ segments=1, width=width)
+ obj.vertex_groups.remove(obj.vertex_groups['corner'])
+
+ def add_sharp_corner(self, obj, x, y):
+ cutter = new_plane(size=LARGE)
+ butil.modify_mesh(cutter, 'SOLIDIFY', offset=0, thickness=1)
+ x_ratio, y_ratio = uniform(.1, .3, 2)
+ cutter.location = x + (LARGE / 2 - unit_cast(x_ratio * self.width)) * (-1) ** (x <= 0), y + (
+ LARGE / 2 - unit_cast(y_ratio * self.height)) * (-1) ** (y <= 0), 0
+ butil.modify_mesh(obj, 'BOOLEAN', object=cutter, operation='DIFFERENCE')
+ butil.delete(cutter)
+
+ def add_long_corner(self, obj, x, y, axis):
+ x_, y_, z_ = read_co(obj).T
+ i = np.nonzero((x_ == x) & (y_ == y))[0]
+ if axis == 0:
+ y_[i] -= self.height * uniform(.1, .3) * (-1) ** (y_[i] <= 0)
+ else:
+ x_[i] -= self.width * uniform(.1, .3) * (-1) ** (x_[i] <= 0)
+ write_co(obj, np.stack([x_, y_, z_], -1))
+
+ def add_staircase(self, contour):
+ x, y = contour.boundary.xy
+ x_, x__ = np.min(x), np.max(x)
+ y_, y__ = np.min(y), np.max(y)
+ for _ in range(self.n_trials):
+ area = TYPICAL_AREA_ROOM_TYPES[RoomType.Staircase] * uniform(1.4, 1.6)
+ skewness = log_uniform(.6, .8)
+ if uniform() < .5:
+ skewness = 1 / skewness
+ width, height = unit_cast(np.sqrt(area * skewness).item()), unit_cast(
+ np.sqrt(area / skewness).item())
+ x = unit_cast(uniform(x_, x__ - width))
+ y = unit_cast(uniform(y_, y__ - height))
+ b = box(x, y, x + width, y + height)
+ if contour.contains(b):
+ return b
+ else:
+ raise ValueError('Invalid staircase')
diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py
new file mode 100644
index 000000000..2d26e0db9
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/decorate.py
@@ -0,0 +1,360 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+import logging
+
+import bmesh
+import bpy
+import gin
+import numpy as np
+import shapely
+import trimesh.convex
+from numpy.random import uniform
+from shapely import Point
+from shapely.ops import nearest_points
+from tqdm import trange
+from trimesh.transformations import translation_matrix
+import shapely.affinity
+
+from infinigen.assets.elements import PillarFactory, random_staircase_factory
+from infinigen.assets.materials import plaster, tile
+from infinigen.assets.utils.decorate import read_area, read_co, read_edge_direction, read_edge_length, \
+ read_edges, remove_edges, remove_faces, remove_vertices
+from infinigen.assets.utils.object import obj2trimesh
+from infinigen.assets.windows import WindowFactory
+from infinigen.assets.elements.doors import random_door_factory
+
+from infinigen.core.constraints.example_solver.room.configs import PILLAR_ROOM_TYPES, ROOM_FLOORS, ROOM_WALLS
+from infinigen.core.constraints.example_solver.room.constants import DOOR_WIDTH, WALL_HEIGHT, WALL_THICKNESS
+from infinigen.core.constraints.example_solver.room.types import RoomType, get_room_level
+from infinigen.core.constraints.example_solver import state_def
+
+from infinigen.core.util.blender import deep_clone_obj
+import infinigen.core.constraints.example_solver.room.constants as constants
+from infinigen.core.constraints.example_solver.room.types import get_room_type
+from infinigen.core.util.random import random_general as rg
+
+from infinigen.core import tags as t, tagging
+
+from infinigen.core.constraints import constraint_language as cl
+from infinigen.core.util import blender as butil
+
+logger = logging.getLogger(__name__)
+
+def split_rooms(rooms_meshed: list[bpy.types.Object]):
+
+ extract_tags = {
+ 'wall': {t.Subpart.Wall, t.Subpart.Visible},
+ 'floor': {t.Subpart.SupportSurface, t.Subpart.Visible},
+ 'ceiling': {t.Subpart.Ceiling, t.Subpart.Visible},
+ }
+
+ meshes = {
+ n: [
+ tagging.extract_tagged_faces(r, tags)
+ for r in rooms_meshed
+ ]
+ for n, tags in extract_tags.items()
+ }
+
+ for k, ms in meshes.items():
+ m2delete = []
+ for m in ms:
+ if m.name.startswith('vert'):
+ butil.select_none()
+ butil.delete(m)
+ m2delete.append(m)
+ for m in m2delete:
+ ms.remove(m)
+
+ meshes['exterior'] = [tagging.extract_mask(r, 1 - tagging.tagged_face_mask(r, t.Subpart.Visible)) for r in rooms_meshed]
+
+ for n, objs in meshes.items():
+ for o in objs:
+ o.name = o.name.split('.')[0] + f'.{n}'
+ butil.origin_set(objs, 'ORIGIN_GEOMETRY', center='MEDIAN')
+
+ meshes = {
+ n: butil.put_in_collection(objs, 'unique_assets:room_' + n)
+ for n, objs in meshes.items()
+ }
+
+ return meshes
+
+
+def room_walls(wall_objs: list[bpy.types.Object]):
+
+ wall_fns = list(rg(ROOM_WALLS[get_room_type(r.name)]) for r in wall_objs)
+
+ logger.debug(f'{room_walls.__name__} adding materials to {len(wall_objs)=}, using {len(wall_fns)=}')
+
+ for wall_fn in set(wall_fns):
+ rooms_ = [o for o, w in zip(wall_objs, wall_fns) if w == wall_fn]
+ shape = np.random.choice(['square', 'rectangle', 'hexagon'])
+ kwargs = dict(vertical=True, alternating=False, shape=shape)
+ if wall_fn in [tile, plaster]:
+ indices = np.random.randint(0, 3, len(rooms_))
+ for i in range(3):
+ rooms__ = [r for r, j in zip(rooms_, indices) if j == i]
+ wall_fn.apply(rooms__, **kwargs)
+ else:
+ wall_fn.apply(rooms_, **kwargs)
+
+
+def room_ceilings(ceilings: list[bpy.types.Object]):
+ logger.debug(f'{room_ceilings.__name__} adding materials to {len(ceilings)=}')
+ plaster.apply(ceilings, t.Subpart.Ceiling)
+
+
+def room_floors(floors: list[bpy.types.Object]):
+ floor_fns = list(rg(ROOM_FLOORS[get_room_type(r.name)]) for r in floors)
+ logger.debug(f'{room_floors.__name__} adding materials to {len(floors)=}, using {len(floor_fns)=}')
+ for floor_fn in set(floor_fns):
+ rooms_ = [o for o, f in zip(floors, floor_fns) if f == floor_fn]
+
+ if floor_fn in [tile, plaster]:
+ indices = np.random.randint(0, 3, len(rooms_))
+ for i in range(3):
+ rooms__ = [r for r, j in zip(rooms_, indices) if j == i]
+ floor_fn.apply(rooms__)
+ else:
+ floor_fn.apply(rooms_)
+
+
+@gin.configurable
+def populate_doors(
+ placeholders: list[bpy.types.Object],
+ n_doors=3,
+ door_chance=1,
+ casing_chance=0.0,
+ all_open=False
+):
+
+ factories = [random_door_factory()(np.random.randint(1e7)) for _ in range(3)]
+
+ logger.debug(f'{populate_doors.__name__} populating {len(placeholders)=} with {n_doors=} and {len(factories)=}')
+
+ indices = np.random.randint(0, len(factories), len(placeholders))
+ col = butil.get_collection('unique_assets:doors')
+ casing_col = butil.get_collection('unique_assets:door_casings')
+
+ for i in trange(n_doors, desc='Placing doors'):
+ factory = factories[i]
+ casing_factory = factory.casing_factory
+ doors, casings = [], []
+ for j in np.nonzero(indices == i)[0]:
+
+ if uniform() > door_chance:
+ continue
+ if all_open:
+ rot_z = uniform(0.93, 1.93)
+ else:
+ rot_p = uniform()
+ if rot_p < 0.5:
+ rot_z = uniform(0, 0.1)
+ elif rot_p < 0.7:
+ rot_z = uniform(0.93, 1.03)
+ else:
+ rot_z = uniform(0, 1)
+ rot_z *= np.pi / 2
+
+ door = factory(int(j))
+ door.parent = placeholders[j]
+ door.location = constants.DOOR_WIDTH / 2, constants.WALL_THICKNESS / 2, -constants.DOOR_SIZE / 2
+ door.rotation_euler[-1] = -rot_z
+ doors.append(door)
+
+ if uniform() > casing_chance:
+ continue
+
+ casing = casing_factory(int(j))
+ casing.parent = placeholders[j]
+ casing.location = 0, 0, -constants.DOOR_SIZE / 2
+ casings.append(casing)
+
+ factory.finalize_assets(doors)
+ butil.put_in_collection(doors, col)
+
+ casing_factory.finalize_assets(casings)
+ butil.put_in_collection(casings, casing_col)
+
+
+def populate_windows(placeholders: list[bpy.types.Object], n_windows=1):
+
+ factories = [WindowFactory(np.random.randint(1e5)) for _ in range(n_windows)]
+
+ logger.debug(f'{populate_windows.__name__} populating {len(placeholders)=} with {n_windows=} and {len(factories)=}')
+
+ indices = np.random.randint(0, len(factories), len(placeholders))
+ col = butil.get_collection('unique_assets:windows')
+ for i in range(n_windows):
+ factory = factories[i]
+ windows = []
+ for j in np.nonzero(indices == i)[0]:
+ cutter_dims = placeholders[j].dimensions
+ dims = cutter_dims[0], cutter_dims[2], cutter_dims[1] * uniform(.1, .2)
+ window = factory(int(j), dimensions=dims)
+ window.parent = placeholders[j]
+ window.location[1] = -WALL_THICKNESS / 2
+ window.rotation_euler[1] = np.pi
+ butil.put_in_collection(list(butil.iter_object_tree(window)), col)
+ factory.finalize_assets(windows)
+
+
+def room_stairs(state, rooms_meshed):
+
+ col = butil.get_collection('unique_assets:staircases')
+ states = list(s for k, s in state.objs.items() if get_room_type(k) == RoomType.Staircase)
+ contours, doors = [], []
+ for s in states:
+ doors_ = [bpy.data.objects[k] for k, o in state.objs.items() if any(
+ r.relation == cl.CutFrom() and r.target_name == s.obj.name for r in o.relations) and k.startswith(
+ 'door')]
+ contour = shapely.simplify(s.contour.buffer(-WALL_THICKNESS / 2, join_style='mitre'), .1)
+ for door in doors_:
+ box = shapely.box(-DOOR_WIDTH / 2, -DOOR_WIDTH * 2, DOOR_WIDTH / 2, DOOR_WIDTH * 2)
+ box = shapely.affinity.translate(shapely.affinity.rotate(box, door.rotation_euler[-1]),
+ *door.location)
+ contour = contour.difference(box)
+ doors.append(doors_)
+ contours.append(contour)
+ geoms = []
+ for c, c_ in zip(contours[:-1], contours[1:]):
+ geom = c.intersection(c_)
+ if not geom.geom_type == 'Polygon':
+ geom = sorted(list(g for g in geom.geoms if g.geom_type == 'Polygon'), key=lambda _: _.area)[-1]
+ geoms.append(geom)
+ placeholders, offsets, fns = [], [], []
+ for _ in trange(100, desc='Generating staircases'):
+ butil.delete(placeholders)
+ fns = [random_staircase_factory()(np.random.randint(1e7)) for _ in geoms]
+ placeholders, mlss, lower, upper = [], [], [], []
+ for j, fn in enumerate(fns):
+ ph = fn.create_placeholder(i=np.random.randint(1e7))
+ placeholders.append(ph)
+ polygon = shapely.intersection_all(list(
+ shapely.affinity.translate(geoms[j], -x, -y) for x in [ph.bound_box[0][0], ph.bound_box[-1][0]]
+ for y in [ph.bound_box[0][1], ph.bound_box[-1][1]]))
+ mlss.append(polygon.boundary if polygon.geom_type == 'Polygon' else shapely.MultiLineString(
+ [p.boundary for p in polygon.geoms]))
+ x, y, z = read_co(ph).T
+ lower.append((x[z < WALL_HEIGHT], y[z < WALL_HEIGHT]))
+ upper.append((x[z >= WALL_HEIGHT], y[z >= WALL_HEIGHT]))
+ if any(p.is_empty for p in mlss):
+ continue
+ for _ in range(100):
+ offsets = []
+ for j, mls in enumerate(mlss):
+ p = mls.bounds
+ x = uniform(p[0], p[2])
+ y = uniform(p[1], p[3])
+ p = Point(x, y)
+ projected = nearest_points(mls, p)[0]
+ if max(np.abs(p.x - projected.x), np.abs(p.y - projected.y)) < constants.STAIRCASE_SNAP:
+ p = projected
+ coords = mls.coords if mls.geom_type == 'LineString' else np.concatenate(
+ [ls.coords for ls in mls.geoms])
+ projected = nearest_points(shapely.MultiPoint(coords), Point(x, y))[0]
+ if max(np.abs(p.x - projected.x), np.abs(p.y - projected.y)) < constants.STAIRCASE_SNAP:
+ p = projected
+ x, y = p.x, p.y
+ placeholders[j].location = x, y, j * WALL_HEIGHT + WALL_THICKNESS / 2
+ contains_lower = shapely.contains_xy(contours[j], lower[j][0] + x, lower[j][1] + y).all()
+ contains_upper = shapely.contains_xy(contours[j + 1], upper[j][0] + x, upper[j][1] + y).all()
+ lower_valid = fns[j].valid_contour((x, y), contours[j], doors[j])
+ upper_valid = fns[j].valid_contour((x, y), contours[j + 1], doors[j + 1], False)
+ if not (contains_lower and contains_upper and lower_valid and upper_valid):
+ break
+ offsets.append((x, y))
+ if len(offsets) == len(geoms):
+ ts = list(trimesh.convex.convex_hull(
+ obj2trimesh(ph).apply_transform(translation_matrix([*o, WALL_HEIGHT * j]))) for j, (ph, o)
+ in enumerate(zip(placeholders, offsets)))
+ if all(t.intersection(t_).is_empty for t, t_ in zip(ts[:-1], ts[1:])):
+ break
+ if len(offsets) == len(geoms):
+ break
+ butil.delete(placeholders)
+ for j, fn in enumerate(fns):
+ s = fn(i=np.random.randint(1e7))
+ butil.apply_transform(s, True)
+ s.location = *offsets[j], j * WALL_HEIGHT + WALL_THICKNESS / 2
+ butil.put_in_collection(s, col)
+ cutter = fn.create_cutter(i=np.random.randint(1e7))
+ cutter.location = *offsets[j], j * WALL_HEIGHT + WALL_THICKNESS / 2
+ for mesh in rooms_meshed:
+ if get_room_type(mesh.name) == RoomType.Staircase:
+ level = get_room_level(mesh.name)
+ if level == j + 1:
+ butil.modify_mesh(mesh, 'BOOLEAN', object=cutter, operation='DIFFERENCE', use_self=True,
+ use_hole_tolerant=True)
+ butil.delete(cutter)
+ m = deep_clone_obj(mesh)
+ m.location = -offsets[j][0], -offsets[j][1], 0
+ butil.apply_transform(m, True)
+ g = fns[j].make_guardrail(m)
+ g.location = s.location
+ g.location[-1] += WALL_HEIGHT
+ butil.put_in_collection(g, col)
+ return placeholders
+
+
+def room_pillars(state: state_def.State, walls: list[bpy.types.Object]):
+
+ col = butil.get_collection('pillars')
+
+ pillar_rooms = [
+ s for k, s in state.objs.items()
+ if get_room_type(k) in PILLAR_ROOM_TYPES
+ ]
+
+ for s in pillar_rooms:
+ factory = PillarFactory(np.random.randint(1e7))
+ mesh = next(m for m in walls if m.name.startswith(s.obj.name.split('.')[0]))
+ interior = tagging.extract_tagged_faces(mesh, {t.Subpart.Interior})
+ remove_faces(interior, read_area(interior) < WALL_THICKNESS / 2 * WALL_HEIGHT)
+ selection = (read_edge_length(interior) > WALL_HEIGHT / 2) & (
+ np.abs(read_edge_direction(interior))[:, -1] > .9)
+ selection_ = np.bincount(read_edges(interior)[selection].reshape(-1),
+ minlength=len(interior.data.vertices))
+ remove_vertices(interior, selection_ == 0)
+ remove_vertices(interior, lambda x, y, z: z > WALL_THICKNESS)
+ remove_edges(interior, read_edge_length(interior) < WALL_THICKNESS)
+ interiors = butil.split_object(interior)
+ for i in interiors:
+ with butil.ViewportMode(i, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.dissolve_limited()
+ bm = bmesh.from_edit_mesh(i.data)
+ geom = [v for v in bm.verts if len(v.link_edges) < 2]
+ bmesh.ops.delete(bm, geom=geom)
+ interiors_ = [i for i in interiors if len(i.data.vertices) > 0]
+ butil.delete([i for i in interiors if len(i.data.vertices) == 0])
+
+ if len(interiors_) == 0:
+ return
+
+ with butil.Suppress():
+ interior = butil.join_objects(interiors_)
+
+ staircases = list(butil.get_collection('staircases').objects)
+ if len(staircases) == 0:
+ return
+
+ staircases = np.concatenate([
+ read_co(o) + np.array([o.location]) for o in staircases
+ ])
+ cos = read_co(interior)
+ cos[:, -1] = mesh.location[-1] + WALL_THICKNESS / 2
+ cos = cos[np.min(np.linalg.norm(cos[:, np.newaxis] - staircases[np.newaxis], axis=-1),
+ -1) > WALL_THICKNESS]
+ for co in cos:
+ obj = factory(np.random.randint(1e7))
+ obj.location = co
+ butil.put_in_collection(obj, col)
+ butil.delete(interior)
diff --git a/infinigen/core/constraints/example_solver/room/graph.py b/infinigen/core/constraints/example_solver/room/graph.py
new file mode 100644
index 000000000..7f21ac06f
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/graph.py
@@ -0,0 +1,167 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from collections import defaultdict, deque
+from collections.abc import Sequence
+
+import gin
+import numpy as np
+import networkx as nx
+from numpy.random import uniform
+
+from infinigen.core.constraints.example_solver.room.utils import unit_cast
+from infinigen.core.util.math import FixedSeed
+from infinigen.core.util.random import log_uniform, random_general as rg
+
+from infinigen.core.constraints.example_solver.room.types import RoomGraph, RoomType, get_room_type
+from infinigen.core.constraints.example_solver.room.configs import (
+ LOOP_ROOM_TYPES, ROOM_CHILDREN,
+ ROOM_NUMBERS,
+ TYPICAL_AREA_ROOM_TYPES, UPSTAIRS_ROOM_CHILDREN, STUDIO_ROOM_CHILDREN,
+)
+
+
+@gin.configurable(denylist=['factory_seed', 'level'])
+class GraphMaker:
+ def __init__(self, factory_seed, level=0, requires_staircase=False, room_children='home', typical_area_room_types=TYPICAL_AREA_ROOM_TYPES,
+ loop_room_types=LOOP_ROOM_TYPES, room_numbers=ROOM_NUMBERS, max_cycle_basis=1,
+ requires_bathroom_privacy=True,
+ entrance_type=('weighted_choice', (.5, 'porch'), (.5, 'hallway')), hallway_alpha=1,
+ no_hallway_children_prob=.4):
+ self.factory_seed = factory_seed
+ with FixedSeed(factory_seed):
+ self.requires_staircase = requires_staircase
+ match room_children:
+ case 'home':
+ self.room_children = ROOM_CHILDREN if level == 0 else UPSTAIRS_ROOM_CHILDREN
+ case _:
+ self.room_children = STUDIO_ROOM_CHILDREN
+ self.hallway_room_types = [r for r, m in self.room_children.items() if RoomType.Hallway in m]
+ self.typical_area_room_types = typical_area_room_types
+ self.loop_room_types = loop_room_types
+ self.room_numbers = room_numbers
+ self.max_samples = 1000
+ self.slackness = log_uniform(1.5, 1.8)
+ self.max_cycle_basis = max_cycle_basis
+ self.requires_bathroom_privacy = requires_bathroom_privacy
+ self.entrance_type = rg(entrance_type)
+ self.hallway_prob = lambda x: 1 / (x + hallway_alpha)
+ self.no_hallway_children_prob = no_hallway_children_prob
+ self.skewness_min = .7
+
+ def make_graph(self, i):
+ with FixedSeed(i):
+ for _ in range(self.max_samples):
+ room_type_counts = defaultdict(int)
+ rooms = []
+ children = defaultdict(list)
+ queue = deque()
+
+ def add_room(t, p):
+ i = len(rooms)
+ name = f'{t}_{room_type_counts[t]}'
+ room_type_counts[t] += 1
+ if p is not None:
+ children[p].append(i)
+ rooms.append(name)
+ queue.append(i)
+
+ add_room(RoomType.LivingRoom, None)
+ while len(queue) > 0:
+ i = queue.popleft()
+ for rt, spec in self.room_children[get_room_type(rooms[i])].items():
+ for _ in range(rg(spec)):
+ add_room(rt, i)
+
+ if self.requires_bathroom_privacy and not self.has_bathroom_privacy:
+ continue
+
+ for i, r in enumerate(rooms):
+ for j, s in enumerate(rooms):
+ if (rt := get_room_type(r)) in self.loop_room_types:
+ if (rt_ := get_room_type(s)) in self.loop_room_types[rt]:
+ if uniform() < self.loop_room_types[rt][rt_] and j not in children[i]:
+ children[i].append(j)
+
+ for i, r in enumerate(rooms):
+ if get_room_type(r) in self.hallway_room_types:
+ hallways = [j for j in children[i] if get_room_type(rooms[j]) == RoomType.Hallway]
+ other_rooms = [j for j in children[i] if get_room_type(rooms[j]) != RoomType.Hallway]
+ children[i] = hallways.copy()
+ for k, o in enumerate(other_rooms):
+ if uniform() < self.no_hallway_children_prob or len(hallways) == 0:
+ children[i].append(o)
+ else:
+ children[hallways[np.random.randint(len(hallways))]].append(o)
+
+ hallways = [i for i, r in enumerate(rooms) if get_room_type(r) == RoomType.Hallway]
+ if len(hallways) == 0:
+ entrance = 0
+ else:
+ if self.requires_staircase:
+ prob = np.array([self.hallway_prob(len(children[h])) for h in hallways])
+ add_room(RoomType.Staircase, np.random.choice(hallways, p=prob / prob.sum()))
+ prob = np.array([self.hallway_prob(len(children[h])) for h in hallways])
+ entrance = np.random.choice(hallways, p=prob / prob.sum())
+ if self.entrance_type == 'porch':
+ add_room(RoomType.Balcony, entrance)
+ entrance = queue.pop()
+ elif self.entrance_type == 'none':
+ entrance = None
+
+ children_ = [children[i] for i in range(len(rooms))]
+ room_graph = RoomGraph(children_, rooms, entrance)
+ if self.satisfies_constraint(room_graph):
+ return room_graph
+
+ __call__ = make_graph
+
+ def satisfies_constraint(self, graph):
+ if not graph.is_planar or len(graph.cycle_basis) > self.max_cycle_basis:
+ return False
+ for room_type, constraint in self.room_numbers.items():
+ if isinstance(constraint, Sequence):
+ n_min, n_max = constraint
+ else:
+ n_min, n_max = constraint, constraint
+ if not n_min <= len(graph[room_type]) <= n_max:
+ return False
+ return True
+
+ def has_bathroom_privacy(self, rooms, children):
+ for i, r in rooms:
+ if get_room_type(r) == RoomType.LivingRoom:
+ has_public_bathroom = any(get_room_type(rooms[j]) == RoomType.Bathroom for j in children[i])
+ if not has_public_bathroom:
+ for j in children[i]:
+ if get_room_type(rooms[j] == RoomType.Bedroom):
+ if not any(get_room_type(rooms[k]) for k in children[j]):
+ return False
+ return True
+
+ def suggest_dimensions(self, graph, width=None, height=None):
+ area = sum([self.typical_area_room_types[get_room_type(r)] for r in graph.rooms]) * self.slackness
+ if width is None and height is None:
+ skewness = uniform(self.skewness_min, 1 / self.skewness_min)
+ width = unit_cast(np.sqrt(area * skewness).item())
+ height = unit_cast(np.sqrt(area / skewness).item())
+ elif uniform(0, 1) < .5:
+ height_ = unit_cast(area / width)
+ height = None if height_ > height and self.skewness_min < height_ / width < 1 / self.skewness_min\
+ else height_
+ else:
+ width_ = unit_cast(area / height)
+ width = None if width_ > width and self.skewness_min < width_ / height < 1 / self.skewness_min \
+ else width_
+
+ return width, height
+
+ def draw(self, graph):
+ g = nx.Graph()
+ shortnames = [r[:3].upper() + r.split('_')[-1] for r in graph.rooms]
+ g.add_nodes_from(shortnames)
+ for k in range(len(shortnames)):
+ for l in graph.neighbours[k]:
+ g.add_edge(shortnames[k], shortnames[l])
+ nx.draw_planar(g, with_labels=True)
diff --git a/infinigen/core/constraints/example_solver/room/scorer.py b/infinigen/core/constraints/example_solver/room/scorer.py
new file mode 100644
index 000000000..c37eb1351
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/scorer.py
@@ -0,0 +1,281 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+from collections import defaultdict
+
+import gin
+import numpy as np
+from shapely import LineString, Polygon
+
+import infinigen.core.constraints.example_solver.room.constants as constants
+from infinigen.core.constraints.example_solver.room.types import RoomType, get_room_type
+from infinigen.core.constraints.example_solver.room.configs import EXTERIOR_CONNECTED_ROOM_TYPES, FUNCTIONAL_ROOM_TYPES, SQUARE_ROOM_TYPES, \
+ TYPICAL_AREA_ROOM_TYPES
+from infinigen.core.constraints.example_solver.room.utils import abs_distance, buffer, unit_cast
+
+
+@gin.configurable(denylist=['graph'])
+class BlueprintScorer:
+ def __init__(
+ self,
+ graph,
+ shortest_path_weight=2.,
+ typical_area_weight=10.,
+ typical_area_room_types=TYPICAL_AREA_ROOM_TYPES,
+ aspect_ratio_weight=10.,
+ aspect_ratio_room_types=SQUARE_ROOM_TYPES,
+ convexity_weight=50.,
+ conciseness_weight=2.,
+ exterior_connected_room_types=EXTERIOR_CONNECTED_ROOM_TYPES,
+ exterior_length_weight=.2,
+ exterior_corner_weight=.02,
+ collinearity_weight=.02,
+ functional_room_weight=.2,
+ functional_room_types=FUNCTIONAL_ROOM_TYPES,
+ narrow_passage_weight=5.,
+ narrow_passage_thresh=1.5
+ ):
+ self.graph = graph
+ self.shortest_path_weight = shortest_path_weight
+
+ self.typical_area_weight = typical_area_weight
+ self.typical_area_room_types = typical_area_room_types
+
+ self.aspect_ratio_weight = aspect_ratio_weight
+ self.aspect_ratio_room_types = aspect_ratio_room_types
+
+ self.convexity_weight = convexity_weight
+
+ self.conciseness_weight = conciseness_weight
+ self.conciseness_thresh = 4
+
+ self.exterior_length_weight = exterior_length_weight
+ self.exterior_connected_room_types = exterior_connected_room_types
+ self.exterior_corner_weight = exterior_corner_weight
+
+ self.collinearity_weight = collinearity_weight
+
+ self.functional_room_weight = functional_room_weight
+ self.functional_room_types = functional_room_types
+
+ self.narrow_passage_weight = narrow_passage_weight
+ self.narrow_passage_thresh = narrow_passage_thresh
+
+ def find_score(self, assignment, info):
+ return sum(self.compute_scores(assignment, info).values())
+
+ def compute_scores(self, assignment, info):
+ info['neighbours'] = {a: set(assignment[_] for _ in self.graph.neighbours[i]) for i, a in
+ enumerate(assignment)}
+ scores = {}
+ if self.shortest_path_weight > 0:
+ score = self.shortest_path_weight * self.shortest_path(assignment, info)
+ scores['shortest_path'] = score
+ if self.typical_area_weight > 0:
+ score = self.typical_area_weight * self.typical_area(assignment, info)
+ scores['typical_area'] = score
+ if self.aspect_ratio_weight > 0:
+ score = self.aspect_ratio_weight * self.aspect_ratio(assignment, info)
+ scores['aspect_ratio'] = score
+ if self.convexity_weight > 0:
+ score = self.convexity_weight * self.convexity(assignment, info)
+ scores['convexity'] = score
+ if self.conciseness_weight > 0:
+ score = self.conciseness_weight * self.conciseness(assignment, info)
+ scores['conciseness'] = score
+ if self.exterior_length_weight > 0:
+ score = self.exterior_length_weight * self.exterior_length(assignment, info)
+ scores['exterior_length'] = score
+ if self.exterior_corner_weight > 0:
+ score = self.exterior_corner_weight * self.exterior_corner(assignment, info)
+ scores['exterior_corner'] = score
+ if self.collinearity_weight > 0:
+ score = self.collinearity_weight * self.collinearity(assignment, info)
+ scores['collinearity'] = score
+ if self.functional_room_weight > 0:
+ score = self.functional_room_weight * self.functional_room(assignment, info)
+ scores['functional_room'] = score
+ if self.narrow_passage_weight > 0:
+ score = self.narrow_passage_weight * self.narrow_passage(assignment, info)
+ scores['narrow_passage'] = score
+ return scores
+
+ def shortest_path(self, assignment, info):
+ shortest_paths = defaultdict(dict)
+ centroids = {k: s.centroid.coords[:][0] for k, s in info['segments'].items()}
+ for k, ses in info['shared_edges'].items():
+ for l, se in ses.items():
+ min_distance = np.full(100, 4)
+ for ls in se.geoms:
+ for c in ls.coords[:]:
+ dist = abs_distance(centroids[k], c) + abs_distance(c, centroids[l])
+ if np.sum(dist) <= np.sum(min_distance):
+ min_distance = dist
+ shortest_paths[k][l] = min_distance
+ roots = self.graph[RoomType.Staircase]
+ if self.graph.entrance is not None:
+ roots.append(self.graph.entrance)
+ scores = {}
+ for root in roots:
+ root = assignment[root]
+ displacement = {a: np.array([1e3] * 4) for a in assignment}
+ displacement[root] = np.zeros(4)
+ updated = True
+ while updated:
+ updated = False
+ for k, ns in info['neighbours'].items():
+ for n in ns:
+ d = displacement[k] + shortest_paths[k][n]
+ if np.sum(d) < np.sum(displacement[n]):
+ displacement[n] = d
+ updated = True
+ displacements = np.stack([d for k, d in displacement.items() if k != root])
+ x, xx, y, yy = displacements.T
+ score = (1. / ((np.maximum(x, xx) + np.maximum(y, yy)) / displacements.sum(1)) - 1) ** 2
+ scores[root] = score.sum()
+ return sum(s for s in scores.values())
+
+ def typical_area(self, assignment, info):
+ total_typical_areas, total_face_areas = [], []
+ for i, r in enumerate(self.graph.rooms):
+ if get_room_type(r) in self.typical_area_room_types:
+ total_typical_areas.append(self.typical_area_room_types[get_room_type(r)])
+ total_face_areas.append(info['segments'][assignment[i]].area)
+ total_typical_areas = np.array(total_typical_areas)
+ total_face_areas = np.array(total_face_areas)
+ scores = total_face_areas / np.sum(total_face_areas) / total_typical_areas * np.sum(total_typical_areas)
+ scores = np.where(scores > 1, scores, 1 / scores) - 1
+ return scores.sum()
+
+ def aspect_ratio(self, assignment, info):
+ aspect_ratios = []
+ for i, r in enumerate(self.graph.rooms):
+ if get_room_type(r) in self.aspect_ratio_room_types:
+ x, y, xx, yy = info['segments'][assignment[i]].bounds
+ aspect_ratios.append((xx - x) / (yy - y))
+ aspect_ratios = np.array(aspect_ratios)
+ aspect_ratios = np.where(aspect_ratios > 1, aspect_ratios, 1 / aspect_ratios)
+ scores = aspect_ratios - 1
+ return scores.sum()
+
+ def convexity(self, assignment, info):
+ sharpness = []
+ for s in info['segments'].values():
+ sharpness.append(s.convex_hull.area / s.area)
+ sharpness = np.array(sharpness)
+ scores = (sharpness - 1) ** 2
+ return scores.sum()
+
+ def conciseness(self, assignment, info):
+ conciseness = np.array([len(s.boundary.coords) - 1 for s in info['segments'].values()])
+ scores = (conciseness / self.conciseness_thresh - 1) ** 2
+ return scores.sum()
+
+ def exterior_length(self, assignment, info):
+ exterior_edges = info['exterior_edges']
+ total_length = 0
+ for i, r in enumerate(self.graph.rooms):
+ if get_room_type(r) in self.exterior_connected_room_types:
+ if assignment[i] in exterior_edges:
+ total_length += exterior_edges[assignment[i]].length
+ score = total_length / sum(ee.length for ee in exterior_edges.values())
+ return (score - 1) ** 2 * len(info['segments'])
+
+ def exterior_corner(self, assignment, info):
+ exterior_edges = info['exterior_edges']
+ total_corners, corners = 0, 0
+ for i, r in enumerate(self.graph.rooms):
+ if assignment[i] in exterior_edges:
+ ee = exterior_edges[assignment[i]]
+ for e in [ee] if isinstance(ee, LineString) else ee.geoms:
+ n = len(e.coords[:]) - 2
+ corners += n
+ if get_room_type(r) in self.exterior_connected_room_types:
+ total_corners += n
+ score = total_corners / corners
+ return (score - 1) ** 2 * len(info['segments'])
+
+ def collinearity(self, assignment, info):
+ x_skeletons, y_skeletons = set(), set()
+ for s in info['segments'].values():
+ x, y = s.boundary.xy
+ for i in range(len(x) - 1):
+ if np.abs(x[i] - x[i + 1]) < 1e-2:
+ x_skeletons.add(unit_cast(x[i]))
+ elif np.abs(y[i] - y[i + 1]) < 1e-2:
+ y_skeletons.add(unit_cast(y[i]))
+ score = len(x_skeletons) + len(y_skeletons)
+ return score * len(info['segments'])
+
+ def functional_room(self, assignment, info):
+ total_area = 0
+ segments = info['segments']
+ for i, r in enumerate(self.graph.rooms):
+ if get_room_type(r) in self.functional_room_types:
+ total_area += segments[assignment[i]].area
+ score = total_area / sum(s.area for s in segments.values())
+ return (1 - score) ** 2 * len(info['segments'])
+
+ def narrow_passage(self, assignment, info):
+ scores = []
+ for p in info['segments'].values():
+ for d in np.arange(1, int(self.narrow_passage_thresh / constants.UNIT)):
+ with np.errstate(invalid="ignore"):
+ length = d * constants.UNIT / 2
+ b = buffer(p, -length)
+ c = buffer(b, length)
+ scores.append(p.area - c.area + (
+ self.narrow_passage_thresh ** 2 * 20 if not isinstance(b, Polygon) else 0))
+ scores = np.array(scores).sum()
+ return scores
+
+
+@gin.configurable(denylist=['graphs'])
+class JointBlueprintScorer:
+ def __init__(self, graphs, *args, staircase_occupancy_weight=1., staircase_iou_weight=.5, **kwargs):
+ self.scorers = []
+ self.graphs = graphs
+ for g in self.graphs:
+ self.scorers.append(BlueprintScorer(g, *args, **kwargs))
+ self.staircase_occupancy_weight = staircase_occupancy_weight
+ self.staircase_iou_weight = staircase_iou_weight
+
+ def compute_scores(self, assignments, infos):
+ scores = {}
+ for i, (assignment, info) in enumerate(zip(assignments, infos)):
+ floor_scores = self.scorers[i].compute_scores(assignment, info)
+ scores.update({f'{k}_{i:01d}': v for k, v in floor_scores.items()})
+ if len(self.graphs) > 1:
+ if self.staircase_occupancy_weight > 0:
+ score = self.staircase_occupancy_weight * self.staircase_occupancy(assignments, infos)
+ scores['staircase_occupancy'] = score
+ if self.staircase_iou_weight > 0:
+ score = self.staircase_iou_weight * self.staircase_iou(assignments, infos)
+ scores['staircase_iou'] = score
+ return scores
+
+ def find_score(self, assignments, infos):
+ return sum(self.compute_scores(assignments, infos).values())
+
+ def staircase_occupancy(self, assignments, infos):
+ scores = []
+ for graph, assignment, info in zip(self.graphs, assignments, infos):
+ for _ in graph[RoomType.Staircase]:
+ scores.append(info['staircase_occupancies'][assignment[_]])
+ scores = np.array(scores)
+ return ((scores - 1) ** 2).sum() * sum(len(info['segments']) for info in infos)
+
+ def staircase_iou(self, assignments, infos):
+ scores = []
+ for graph, assignment, info in zip(self.graphs, assignments, infos):
+ for _ in graph[RoomType.Staircase]:
+ segment = info['segments'][assignment[_]]
+ staircase = info['staircase']
+ scores.append(segment.intersection(staircase).area / segment.union(staircase).area)
+ scores = np.array(scores)
+ return ((scores - 1) ** 2).sum() * sum(len(info['segments']) for info in infos)
diff --git a/infinigen/core/constraints/example_solver/room/segment.py b/infinigen/core/constraints/example_solver/room/segment.py
new file mode 100644
index 000000000..dc02f7962
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/segment.py
@@ -0,0 +1,144 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+from collections import defaultdict
+
+import numpy as np
+import shapely
+from matplotlib import pyplot as plt
+from numpy.random import uniform
+from shapely import LineString, union
+
+from infinigen.assets.utils.shapes import shared
+from infinigen.core.constraints.example_solver.room.utils import compute_neighbours, cut_polygon_by_line, canonicalize, is_valid_polygon, \
+ unit_cast, update_exterior_edges, update_shared_edges, update_staircase_occupancies
+import infinigen.core.constraints.example_solver.room.constants as constants
+from infinigen.core.util.random import log_uniform
+from infinigen.core.util.math import FixedSeed
+
+
+class SegmentMaker:
+ def __init__(self, factory_seed, contour, n, merge_alpha=-1):
+ with FixedSeed(factory_seed):
+ self.contour = contour
+ self.n = n
+ self.n_boxes = int(self.n * uniform(1.4, 1.6))
+
+ self.box_ratio = .3
+ self.min_segment_area = log_uniform(1.5, 2)
+ self.min_segment_size = log_uniform(.5, 1.)
+
+ self.divide_box_fn = lambda x: x.area ** .5
+
+ self.n_box_trials = 200
+ self.merge_fn = lambda x: x ** merge_alpha
+
+ def build_segments(self, staircase=None):
+ while True:
+ try:
+ segments, shared_edges = self.filter_segments()
+ break
+ except:
+ pass
+ exterior_edges = update_exterior_edges(segments, shared_edges)
+ neighbours_all = {k: set(compute_neighbours(se, constants.SEGMENT_MARGIN)) for k, se in shared_edges.items()}
+ exterior_neighbours = set(compute_neighbours(exterior_edges, constants.SEGMENT_MARGIN))
+ staircase_occupancies = update_staircase_occupancies(segments, staircase)
+ return {
+ 'segments': segments,
+ 'shared_edges': shared_edges,
+ 'exterior_edges': exterior_edges,
+ 'neighbours_all': neighbours_all,
+ 'exterior_neighbours': exterior_neighbours,
+ 'staircase_occupancies': staircase_occupancies,
+ 'staircase': staircase,
+ }
+
+ def divide_segments(self):
+ segments = {0: self.contour}
+ for _ in range(self.n_boxes):
+ keys, values = zip(*segments.items())
+ prob = np.array([self.divide_box_fn(v) for v in values])
+ for _ in range(self.n_box_trials):
+ k = np.random.choice(list(keys), p=prob / prob.sum())
+ x, y, xx, yy = segments[k].bounds
+ w, h = xx - x, yy - y
+ r = uniform(.25, .75)
+ line = None
+ if w >= h:
+ w_ = unit_cast(r * w)
+ bound = max(self.box_ratio * h, constants.SEGMENT_MARGIN)
+ if w_ >= bound and w - w_ >= bound:
+ line = LineString([(x + w_, -100), (x + w_, 100)])
+ else:
+ h_ = unit_cast(r * h)
+ bound = max(self.box_ratio * w, constants.SEGMENT_MARGIN)
+ if h_ >= bound and h - h_ >= bound:
+ line = LineString([(-100, y + h_), (100, y + h_)])
+ if line is not None:
+ i = max(segments.keys())
+ s, t = cut_polygon_by_line(segments[k], line)
+ s_ = canonicalize(s)
+ t_ = canonicalize(t)
+ if np.abs(s.area - s_.area) < 1e-3 and np.abs(t.area - t_.area) < 1e-3:
+ segments[k], segments[i + 1] = s_, t_
+ break
+ return {k: v for k, v in segments.items()}
+
+ def merge_segment(self, segments, shared_edges, attached, i, j):
+ assert i != j
+ s = canonicalize(union(segments[i], segments[j]))
+ if not is_valid_polygon(s): return
+ segments[j] = s
+ segments.pop(i)
+ shared_edges.pop(i)
+ attached.pop(i)
+ for k, ses in shared_edges.items():
+ if i in ses:
+ ses.pop(i)
+ for k, ats in attached.items():
+ if i in ats:
+ ats.remove(i)
+ for k, s in segments.items():
+ for l, t in segments.items():
+ if k != l and (k == j or l == j):
+ se = shared(s, t)
+ shared_edges[k][l] = se
+ if se.length >= constants.SEGMENT_MARGIN:
+ attached[k].add(l)
+ attached[l].add(k)
+ return shared_edges
+
+ def filter_segments(self):
+ segments = self.divide_segments()
+ shared_edges = defaultdict(dict)
+ attached = defaultdict(set)
+ for k, s in segments.items():
+ for l, t in segments.items():
+ if k < l:
+ se = shared(s, t)
+ shared_edges[k][l] = shared_edges[l][k] = se
+ if se.length >= constants.SEGMENT_MARGIN:
+ attached[k].add(l)
+ attached[l].add(k)
+
+ while len(segments) > self.n:
+ prob = np.array([1 / (len(attached[c]) + 1) for c in shared_edges.keys()])
+ k = np.random.choice(list(shared_edges.keys()), p=prob / prob.sum())
+ candidates = list(k for k, se in shared_edges[k].items() if se.length>=1e-6)
+ prob = np.array([len(attached[c].difference(attached[k]))**2 + .5 for c in candidates])
+ n = np.random.choice(candidates, p=prob / prob.sum())
+ self.merge_segment(segments, shared_edges, attached, k, n)
+ return segments, shared_edges
+
+ def plot(self, segments):
+ plt.clf()
+ for k, s in segments.items():
+ shapely.plotting.plot_polygon(s, color=uniform(0, 1, 3))
+ plt.tight_layout()
+ plt.show()
diff --git a/infinigen/core/constraints/example_solver/room/solidifier.py b/infinigen/core/constraints/example_solver/room/solidifier.py
new file mode 100644
index 000000000..d6eb63637
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/solidifier.py
@@ -0,0 +1,436 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+import logging
+from collections import defaultdict, deque
+from collections.abc import Iterable, Mapping, Sequence
+
+import bmesh
+import bpy
+import gin
+import numpy as np
+from numpy.random import uniform
+from shapely import LineString, line_interpolate_point, remove_repeated_points, simplify
+from shapely.ops import linemerge
+from numpy.random import uniform
+
+from infinigen.core.constraints.example_solver.room.types import RoomGraph, RoomType, get_room_type
+from infinigen.core.constraints.example_solver.room.configs import (
+ COMBINED_ROOM_TYPES, PANORAMIC_ROOM_TYPES,
+ WINDOW_ROOM_TYPES, TYPICAL_AREA_ROOM_TYPES,
+)
+from infinigen.core.constraints.example_solver.room.constants import DOOR_MARGIN, DOOR_SIZE, DOOR_WIDTH, \
+ MAX_WINDOW_LENGTH, SEGMENT_MARGIN, WALL_HEIGHT, WALL_THICKNESS, WINDOW_HEIGHT, WINDOW_SIZE
+
+from infinigen.core.constraints.example_solver.room.utils import SIMPLIFY_THRESH, WELD_THRESH, buffer, \
+ canonicalize, polygon2obj
+from infinigen.assets.utils.decorate import (
+ read_area, read_center, read_co, remove_edges, remove_faces,
+ select_faces, write_attribute, write_co, read_edges, read_edge_direction, read_edge_length,
+)
+from infinigen.assets.utils.object import data2mesh, join_objects, mesh2obj, new_cube, new_line
+from infinigen.core.surface import write_attr_data
+from infinigen.core.tagging import PREFIX
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+from infinigen.core.constraints.example_solver.geometry import parse_scene
+from infinigen.core.constraints.example_solver.state_def import ObjectState, RelationState, State
+from infinigen.core.constraints import constraint_language as cl
+
+from infinigen.assets.utils.autobevel import BevelSharp
+from infinigen.core.util.logging import BadSeedError
+
+logger = logging.getLogger(__name__)
+
+_eps = 0.01
+
+@gin.configurable(denylist=['graph', 'level'])
+class BlueprintSolidifier:
+ def __init__(self, graph: RoomGraph, level, has_ceiling=True, combined_room_types=COMBINED_ROOM_TYPES,
+ panoramic_room_types=PANORAMIC_ROOM_TYPES, enable_open=True):
+ self.graph = graph
+ self.level = level
+ self.has_ceiling = has_ceiling
+ self.combined_room_types = combined_room_types
+ self.panoramic_room_types = panoramic_room_types
+ self.beveler = BevelSharp(mult=10)
+ self.enable_open = enable_open
+
+ def get_entrance(self, names):
+ return None if self.graph.entrance is None else {k for k, n in names.items() if
+ n == self.graph.rooms[self.graph.entrance]}.pop()
+
+ def get_staircase(self, names):
+ return {k for k, n in names.items() if get_room_type(n) == RoomType.Staircase}.pop()
+
+ @staticmethod
+ def unroll(x):
+ for k, cs in x.items():
+ if isinstance(cs, Mapping):
+ for l, c in cs.items():
+ if k < l:
+ yield (k, l), c
+ elif isinstance(cs, Iterable):
+ for c in cs:
+ yield (k,), c
+ else:
+ yield (k,), cs
+
+ def solidify(self, assignment, info):
+ segments = info['segments']
+ neighbours = info['neighbours']
+ shared_edges = info['shared_edges']
+ exterior_edges = info['exterior_edges']
+
+ names = {k: self.graph.rooms[assignment.index(k)] for k in segments}
+ rooms = {k: self.make_room(p, exterior_edges.get(k, None)) for k, p in segments.items()}
+ for k, o in rooms.items():
+ o.name = f'{names[k]}-{self.level}'
+ # if segments[k].area > 2.5 * TYPICAL_AREA_ROOM_TYPES[get_room_type(names[k])] + 5:
+ # raise BadSeedError()
+ #
+
+ open_cutters, door_cutters = self.make_interior_cutters(neighbours, shared_edges, segments, names)
+ exterior_cutters = self.make_exterior_cutters(exterior_edges, names)
+
+ for k, r in rooms.items():
+ r.location[-1] += WALL_HEIGHT * self.level
+ for cutters in [open_cutters, door_cutters, exterior_cutters]:
+ for k, c in self.unroll(cutters):
+ c.location[-1] += WALL_HEIGHT * self.level
+
+ butil.put_in_collection(rooms.values(), 'placeholders:room_shells')
+
+ state = self.convert_solver_state(
+ rooms, segments, shared_edges, open_cutters, door_cutters, exterior_cutters
+ )
+
+ def clone_as_meshed(o):
+ new = butil.copy(o)
+ new.name = o.name + '.meshed'
+ return new
+ rooms = {k: clone_as_meshed(r) for k, r in rooms.items()}
+
+ # Cut windows & doors from final room meshes
+ cutter_col = butil.get_collection('placeholders:portal_cutters')
+ for cutters in [open_cutters, door_cutters, exterior_cutters]:
+ for k, c in self.unroll(cutters):
+ for k_ in k:
+ butil.put_in_collection(c, cutter_col)
+ before = len(rooms[k_].data.polygons)
+ butil.modify_mesh(
+ rooms[k_], 'BOOLEAN', object=c, operation='DIFFERENCE', use_self=True,
+ use_hole_tolerant=True
+ )
+ after = len(rooms[k_].data.polygons)
+ logger.debug(f'Cutting {c.name} from {rooms[k_].name}, {before=} {after=}')
+
+ for r in rooms.values():
+ butil.modify_mesh(r, 'TRIANGULATE', min_vertices=3)
+ remove_faces(r, read_area(r) < 5e-4)
+ with butil.ViewportMode(r, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.dissolve_limited(angle_limit=0.001)
+ x, y, z = read_co(r).T
+ z = np.where(np.abs(z - WALL_THICKNESS / 2) < .01, WALL_THICKNESS / 2, z)
+ z = np.where(np.abs(z - WALL_HEIGHT + WALL_THICKNESS / 2) < .01, WALL_HEIGHT - WALL_THICKNESS / 2,
+ z)
+ write_co(r, np.stack([x, y, z], -1))
+ butil.modify_mesh(r, 'WELD', merge_threshold=WALL_THICKNESS / 10)
+
+ direction = read_edge_direction(r)
+ z_edges = np.abs(direction[:, -1])
+ orthogonal = (z_edges < .1) | (z_edges > .9)
+ with butil.ViewportMode(r, 'EDIT'):
+ edge_faces = np.zeros(len(orthogonal))
+ bm = bmesh.from_edit_mesh(r.data)
+ for f in bm.faces:
+ for e in f.edges:
+ edge_faces[e.index] += 1
+ orthogonal = (z_edges < .1) | (z_edges > .9) | (edge_faces != 1) | (read_edge_length(r) < .5)
+ if not orthogonal.all():
+ raise BadSeedError('No orthogonal edges')
+
+ butil.group_in_collection(rooms.values(), 'placeholders:room_meshes')
+
+ return state, rooms
+
+ def convert_solver_state(self, rooms, segments, shared_edges, open_cutters, door_cutters, exterior_cutters):
+ obj_states = {}
+ for k, o in rooms.items():
+
+ tags = {t.Semantics.Room, t.Semantics(o.name.split('_')[0])}
+
+ tags.add(t.SpecificObject(o.name))
+ obj_states[o.name] = ObjectState(
+ obj=o,
+ tags=tags,
+ contour=segments[k]
+ )
+ for k, r in rooms.items():
+ relations = obj_states[r.name].relations
+ for other in shared_edges[k]:
+ if other in open_cutters[k]:
+ ct = cl.ConnectorType.Open
+ elif other in door_cutters[k]:
+ ct = cl.ConnectorType.Door
+ else:
+ ct = cl.ConnectorType.Wall
+ relations.append(RelationState(
+ cl.RoomNeighbour({ct}), rooms[other].name)
+ )
+
+ cut_state = lambda x: RelationState(cl.CutFrom(), rooms[x].name)
+ for cutters in [door_cutters, open_cutters, exterior_cutters]:
+ for k, c in self.unroll(cutters):
+
+ tags = set({t.Semantics.Cutter, t.SpecificObject(c.name)})
+
+ # TODO Lingjie - do not store whole-object window/door semantics in per-vertex attributes
+ meshtags = tagging.union_object_tags(c)
+ for tag in [t.Semantics.Door, t.Semantics.Window, t.Semantics.Entrance]:
+ if tag.value in meshtags:
+ tags.add(tag)
+
+ if t.Semantics.Door in meshtags:
+ # include full possible swing extent of door in state to prevent objects blocking
+ c.scale.x *= (DOOR_WIDTH + WALL_THICKNESS) / DOOR_WIDTH
+
+ obj_states[c.name] = ObjectState(
+ obj=c,
+ tags=tags,
+ relations=list(cut_state(k_) for k_ in k)
+ )
+
+ return State(objs=obj_states)
+
+ def make_room(self, obj, exterior_edges=None):
+ obj = polygon2obj(canonicalize(obj), True)
+ butil.modify_mesh(obj, "WELD", merge_threshold=.2)
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=WALL_HEIGHT, offset=-1)
+ self.tag(obj, False)
+ if exterior_edges is not None:
+ center = read_center(obj)
+ exterior_centers = []
+ for ls in exterior_edges.geoms if exterior_edges.geom_type == 'MultiLineString' else [
+ exterior_edges]:
+ for u, v in zip(ls.coords[:-1], ls.coords[1:]):
+ exterior_centers.append(((u[0] + v[0]) / 2, (u[1] + v[1]) / 2))
+ exterior = (np.abs(center[:, np.newaxis, :2] - np.array(exterior_centers)[np.newaxis]).sum(
+ -1) < WALL_THICKNESS * 4).any(-1).astype(int)
+ else:
+ exterior = np.zeros(len(obj.data.polygons), dtype=int)
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Exterior.value}', exterior, 'INT', 'FACE')
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Interior.value}', 1 - exterior, 'INT', 'FACE')
+
+ assert len(obj.data.vertices) > 0
+
+ obj.vertex_groups.new(name='visible_')
+ butil.modify_mesh(obj, 'SOLIDIFY', thickness=WALL_THICKNESS / 2, offset=-1, use_even_offset=True,
+ shell_vertex_group='visible_', use_quality_normals=True)
+ write_attribute(obj, 'visible_', f'{PREFIX}{t.Subpart.Visible.value}', 'FACE', 'INT')
+ obj.vertex_groups.remove(obj.vertex_groups['visible_'])
+ tagging.tag_object(obj, t.Semantics.Room)
+ return obj
+
+ def make_interior_cutters(self, neighbours, shared_edges, segments, names):
+ name_groups = {}
+ for k, n in names.items():
+ name_groups[k] = set(i for i, rt in enumerate(self.combined_room_types) if get_room_type(n) in rt)
+ dist2entrance = self.compute_dist2entrance(neighbours, names)
+ centroids = {k: np.array(s.centroid.coords[0]) for k, s in segments.items()}
+ open_cutters, door_cutters = defaultdict(dict), defaultdict(dict)
+ for k, ses in shared_edges.items():
+ for l, se in ses.items():
+ if l not in neighbours[k] or k >= l:
+ continue
+ if len(name_groups[k].intersection(name_groups[l])) > 0 and self.enable_open:
+ open_cutters[k][l] = open_cutters[l][k] = self.make_open_cutter(se)
+ else:
+ direction = (centroids[k] - centroids[l]) * (
+ 1 if dist2entrance[k] > dist2entrance[l] else -1)
+ door_cutters[k][l] = door_cutters[l][k] = self.make_door_cutter(se, direction)
+ return open_cutters, door_cutters
+
+ def compute_dist2entrance(self, neighbours, names):
+ root = self.get_entrance(names)
+ if root is None:
+ root = self.get_staircase(names)
+ queue = deque([root])
+ dist2living_room = {root: 0}
+ while len(queue) > 0:
+ node = queue.popleft()
+ for n in neighbours[node]:
+ if n not in dist2living_room:
+ dist2living_room[n] = dist2living_room[node] + 1
+ queue.append(n)
+ return dist2living_room
+
+ def make_exterior_cutters(self, exterior_edges, names):
+ cutters = defaultdict(list)
+ entrance = self.get_entrance(names)
+
+ for k, mls in exterior_edges.items():
+
+ room_type = get_room_type(names[k])
+ pano_chance = self.panoramic_room_types.get(room_type, 0)
+ is_panoramic = uniform() < pano_chance
+
+ lss = []
+ for ls in mls.geoms:
+ coords = ls.coords[:]
+ lss.extend(list(zip(coords[:-1], coords[1:])))
+ np.random.shuffle(lss)
+ if k == entrance:
+ ls = lss.pop()
+ cutter = self.make_entrance_cutter(ls)
+ cutters[k].append(cutter)
+ for ls in lss:
+ coords = LineString(ls).segmentize(MAX_WINDOW_LENGTH).coords[:]
+ for seg in zip(coords[:-1], coords[1:]):
+ length = np.linalg.norm([seg[1][1] - seg[0][1], seg[1][0] - seg[0][0]])
+ if length >= DOOR_WIDTH + WALL_THICKNESS and uniform() < WINDOW_ROOM_TYPES[
+ get_room_type(names[k])]:
+ cutter = self.make_window_cutter(seg, is_panoramic)
+ cutters[k].append(cutter)
+ return cutters
+
+ def make_staircase_cutters(self, staircase, names):
+ cutters = defaultdict(list)
+ if self.level > 0:
+ for k, name in names.items():
+ if get_room_type(name) == RoomType.Staircase:
+ with np.errstate(invalid="ignore"):
+ cutter = polygon2obj(buffer(staircase, -WALL_THICKNESS / 2))
+ butil.modify_mesh(cutter, 'SOLIDIFY', thickness=WALL_THICKNESS * 1.2, offset=0)
+ self.tag(cutter)
+ cutter.name = 'staircase_cutter'
+ cutters[k].append(cutter)
+ return cutters
+
+ def make_door_cutter(self, es, direction):
+ lengths = [ls.length for ls in es.geoms]
+ (x, y), (x_, y_) = es.geoms[np.argmax(lengths)].coords
+
+ cutter = new_cube()
+ vertical = np.abs(x - x_) < .1
+ cutter.scale = DOOR_WIDTH / 2 * (1 - _eps), DOOR_WIDTH, DOOR_SIZE / 2
+
+ butil.apply_transform(cutter, True)
+ if vertical:
+ y = uniform(min(y, y_) + DOOR_MARGIN, max(y, y_) - DOOR_MARGIN)
+ z_rot = -np.pi / 2 if direction[0] > 0 else np.pi / 2
+ else:
+ x = uniform(min(x, x_) + DOOR_MARGIN, max(x, x_) - DOOR_MARGIN)
+ z_rot = 0 if direction[-1] > 0 else np.pi
+ cutter.location = x, y, DOOR_SIZE / 2 + WALL_THICKNESS / 2 + _eps
+ cutter.rotation_euler[-1] = z_rot
+ tagging.tag_object(cutter, t.Semantics.Door)
+ self.tag(cutter)
+ cutter.name = t.Semantics.Door.value
+ return cutter
+
+ def make_entrance_cutter(self, ls):
+ (x, y), (x_, y_) = ls
+ cutter = new_cube()
+ length = np.linalg.norm([y_ - y, x_ - x])
+ d = (DOOR_WIDTH + WALL_THICKNESS) / 2 / length
+ lam = uniform(d, 1 - d)
+ cutter.scale = DOOR_WIDTH / 2, DOOR_WIDTH / 2, DOOR_SIZE / 2
+ butil.apply_transform(cutter, True)
+ cutter.location = lam * x + (1 - lam) * x_, lam * y + (
+ 1 - lam) * y_, DOOR_SIZE / 2 + WALL_THICKNESS / 2 + _eps
+ cutter.rotation_euler = 0, 0, np.arctan2(y_ - y, x_ - x)
+ self.tag(cutter)
+ tagging.tag_object(cutter, t.Semantics.Entrance)
+ cutter.name = t.Semantics.Entrance.value
+ return cutter
+
+ def make_window_cutter(self, ls, is_panoramic):
+ (x, y), (x_, y_) = ls
+ length = np.linalg.norm([y_ - y, x_ - x])
+ if is_panoramic:
+ x_scale = length / 2 - WALL_THICKNESS / 2
+ lam = 1 / 2
+ z_scale = (WALL_HEIGHT - WALL_THICKNESS) / 2
+ z_loc = z_scale + WALL_THICKNESS / 2
+ else:
+ x_scale = uniform(DOOR_WIDTH / 2, length / 2 - WALL_THICKNESS / 2)
+ m = (x_scale + WALL_THICKNESS / 2) / length
+ lam = uniform(m, 1 - m)
+ z_scale = WINDOW_SIZE / 2
+ z_loc = z_scale + WINDOW_HEIGHT + WALL_THICKNESS / 2
+ cutter = new_cube()
+ cutter.scale = x_scale, WALL_THICKNESS, z_scale
+ butil.apply_transform(cutter)
+ cutter.location = lam * x + (1 - lam) * x_, lam * y + (1 - lam) * y_, z_loc
+ cutter.rotation_euler = 0, 0, np.arctan2(y - y_, x - x_)
+ self.tag(cutter)
+ tagging.tag_object(cutter, t.Semantics.Window)
+ cutter.name = t.Semantics.Window.value
+ return cutter
+
+ def make_open_cutter(self, es):
+ es = remove_repeated_points(simplify(es, SIMPLIFY_THRESH).normalize(), WELD_THRESH)
+ es = linemerge(es) if not isinstance(es, LineString) else es
+ es = [es] if isinstance(es, LineString) else es.geoms
+ lines = []
+ for ls in es:
+ coords = np.array(ls.coords[:])
+ start, end = 0, -1
+ while np.linalg.norm(coords[start] - coords[start + 1]) < SEGMENT_MARGIN:
+ start += 1
+ while np.linalg.norm(coords[end] - coords[end - 1]) < SEGMENT_MARGIN:
+ end -= 1
+ coords = coords[start:end + 1] if end < -1 else coords[start:]
+ if len(coords) < 2:
+ continue
+ coords[0] = line_interpolate_point(LineString(coords[0: 2]), WALL_THICKNESS / 2 + _eps).coords[0]
+ coords[-1] = line_interpolate_point(LineString(coords[-1:-3:-1]), WALL_THICKNESS / 2 + _eps).coords[
+ 0]
+ line = new_line(len(coords) - 1)
+ write_co(line, np.concatenate([coords, np.zeros((len(coords), 1))], -1))
+ lines.append(line)
+ cutter = join_objects(lines)
+ butil.modify_mesh(cutter, 'WELD', merge_threshold=WELD_THRESH)
+ butil.select_none()
+
+ with butil.ViewportMode(cutter, 'EDIT'):
+ bpy.ops.mesh.select_mode(type='EDGE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.extrude_edges_move(
+ TRANSFORM_OT_translate={'value': (0, 0, WALL_HEIGHT - WALL_THICKNESS - 2 * _eps)
+ })
+ bpy.ops.mesh.select_mode(type='FACE')
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.normals_make_consistent(inside=False)
+
+ cutter.location[-1] += WALL_THICKNESS / 2 + _eps
+ butil.apply_transform(cutter, True)
+ butil.modify_mesh(cutter, 'SOLIDIFY', thickness=WALL_THICKNESS * 3, offset=0, use_even_offset=True)
+ self.tag(cutter)
+ tagging.tag_object(cutter, t.Semantics.Open)
+ cutter.name = t.Semantics.Open.value
+ return cutter
+
+ @staticmethod
+ def tag(obj, visible=True):
+ center = read_center(obj) + obj.location
+ ceiling = center[:, -1] > WALL_HEIGHT - WALL_THICKNESS / 2 - .1
+ floor = center[:, -1] < WALL_THICKNESS / 2 + .1
+ wall = ~(ceiling | floor)
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Ceiling.value}', ceiling, 'INT', 'FACE')
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.SupportSurface.value}', floor, 'INT', 'FACE')
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Wall.value}', wall, 'INT', 'FACE')
+ write_attr_data(obj, 'segment_id', np.arange(len(center)), 'INT', 'FACE')
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Visible.value}',
+ np.ones_like(ceiling) if visible else np.zeros_like(ceiling), 'INT', 'FACE')
+ write_attr_data(obj, f'{PREFIX}{t.Subpart.Invisible.value}',
+ np.zeros_like(ceiling) if visible else np.ones_like(ceiling), 'INT', 'FACE')
+ parse_scene.preprocess_obj(obj)
+ tagging.tag_canonical_surfaces(obj)
diff --git a/infinigen/core/constraints/example_solver/room/solver.py b/infinigen/core/constraints/example_solver/room/solver.py
new file mode 100644
index 000000000..c431f933a
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/solver.py
@@ -0,0 +1,285 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import List, Optional
+
+import gin
+import numpy as np
+from numpy.random import uniform
+from shapely import LineString, Polygon, union
+from shapely.ops import shared_paths
+
+from infinigen.core.constraints.example_solver.room.types import RoomType, get_room_type
+from infinigen.core.constraints.example_solver.room.configs import EXTERIOR_CONNECTED_ROOM_TYPES
+from infinigen.core.constraints.example_solver.room.constants import SEGMENT_MARGIN
+import infinigen.core.constraints.example_solver.room.constants as constants
+from infinigen.core.constraints.example_solver.room.utils import compute_neighbours, cut_polygon_by_line, is_valid_polygon, linear_extend_x, \
+ linear_extend_y, canonicalize, update_exterior_edges, update_shared_edges, update_staircase_occupancies
+
+
+@dataclass
+class RoomSolverMsg:
+ status: str
+ index_changed: Optional[List[int]] = None
+
+ @property
+ def is_success(self):
+ return self.status == 'success'
+
+
+@gin.configurable(denylist=['contour', 'graph'])
+class BlueprintSolver:
+ def __init__(self, contour, graph, exterior_connected_room_types=EXTERIOR_CONNECTED_ROOM_TYPES,
+ max_stride=1, staircase_occupancy_thresh=.75):
+ self.contour = contour
+ x, y = self.contour.boundary.xy
+ self.x_min, self.x_max = np.min(x), np.max(x)
+ self.y_min, self.y_max = np.min(y), np.max(y)
+ self.graph = graph
+ self.staircase_occupancy_thresh = staircase_occupancy_thresh
+ self.exterior_connected_room_types = exterior_connected_room_types
+ self.exterior_connected_rooms = set(
+ i for i, r in enumerate(self.graph.rooms) if get_room_type(r) in self.exterior_connected_room_types)
+ if self.graph.entrance is not None:
+ self.exterior_connected_rooms.add(self.graph.entrance)
+ self.staircase_rooms = set(self.graph[RoomType.Staircase])
+ self.max_stride = max_stride
+
+ def find_assignment(self, info):
+ assignment = [0] * len(self.graph.rooms)
+ neighbours_all = info['neighbours_all']
+ exterior_neighbours = info['exterior_neighbours']
+ staircase_occupancies = info['staircase_occupancies']
+ if info['staircase'] is not None:
+ staircase_candidates = list(
+ (k for k, v in staircase_occupancies.items() if v > self.staircase_occupancy_thresh))
+ if len(staircase_candidates) == 0:
+ return None
+ else:
+ staircase_candidates = []
+ unassigned = set(neighbours_all.keys())
+
+ def assign_(i):
+ if i == len(self.graph.rooms):
+ return assignment
+ if i in self.staircase_rooms:
+ candidates = unassigned.intersection(staircase_candidates)
+ elif i in self.exterior_connected_rooms:
+ candidates = unassigned.intersection(exterior_neighbours)
+ else:
+ candidates = unassigned.copy()
+ n_unassigned = len(list(j for j in self.graph.neighbours[i] if j > i))
+ assigned_neighbours = set(assignment[j] for j in self.graph.neighbours[i] if j < i)
+ for n in candidates:
+ if assigned_neighbours.issubset(neighbours_all[n]):
+ if len(neighbours_all[n].intersection(unassigned)) >= n_unassigned:
+ assignment[i] = n
+ unassigned.remove(n)
+ r = assign_(i + 1)
+ if r is not None:
+ return r
+ unassigned.add(n)
+
+ return assign_(0)
+
+ def satisfies_constraints(self, assignment, info):
+ neighbours_all = info['neighbours_all']
+ exterior_neighbours = info['exterior_neighbours']
+ staircase_occupancies = info['staircase_occupancies']
+ for k, ns in enumerate(self.graph.neighbours):
+ for n in ns:
+ if assignment[k] not in neighbours_all[assignment[n]]:
+ return RoomSolverMsg('neighbours unsatisfied', [k, n])
+ if k in self.exterior_connected_rooms:
+ if assignment[k] not in exterior_neighbours:
+ return RoomSolverMsg('exterior neighbours unsatisfied', [k])
+ if get_room_type(self.graph.rooms[k]) == RoomType.Staircase:
+ if staircase_occupancies[assignment[k]] < self.staircase_occupancy_thresh:
+ return RoomSolverMsg('staircase occupancy unsatisfied', [k])
+ return RoomSolverMsg('success')
+
+ def perturb_solution(self, assignment, info):
+ k = np.random.choice(list(info['segments'].keys()))
+ while True:
+ info_ = deepcopy(info)
+ assignment_ = deepcopy(assignment)
+ try:
+ rn = uniform()
+ if rn < 1 / 3:
+ resp = self.extrude_room_out(assignment, info, k)
+ elif rn < 2 / 3:
+ resp = self.extrude_room_in(assignment, info, k)
+ else:
+ resp = self.swap_room(assignment, info, k)
+ except:
+ info, assignment = info_, assignment_
+ else:
+ break
+ if not resp.is_success:
+ return resp
+ for c in resp.index_changed:
+ if not is_valid_polygon(info['segments'][c]):
+ return RoomSolverMsg('invalid segment', [c])
+ try:
+ for c in resp.index_changed:
+ update_shared_edges(info['segments'], info['shared_edges'], c)
+ update_exterior_edges(info['segments'], info['shared_edges'], info['exterior_edges'], c)
+ update_staircase_occupancies(info['segments'], info['staircase'], info['staircase_occupancies'],
+ c)
+ except:
+ return RoomSolverMsg('Exception')
+ info['neighbours_all'] = {k: set(compute_neighbours(se, SEGMENT_MARGIN)) for k, se in
+ info['shared_edges'].items()}
+ info['exterior_neighbours'] = set(compute_neighbours(info['exterior_edges'], SEGMENT_MARGIN))
+ for k, s in info['segments'].items():
+ x, y = np.array(s.boundary.coords).T
+ if np.any((x < -1.) | (y < -1.) | (x > 40.) | (y > 40.)):
+ return RoomSolverMsg('OOB')
+ satisfies = self.satisfies_constraints(assignment, info)
+ if not satisfies.is_success:
+ return satisfies
+ return resp
+
+ def extrude_room(self, i, info, out=True):
+ segments = info['segments']
+ coords = canonicalize(segments[i]).boundary.coords[:]
+ indices = []
+ for k in range(len(coords) - 1):
+ (x, y), (x_, y_) = coords[k:k + 2]
+ if np.abs(x - x_) < 1e-2 and self.x_min < x < self.x_max:
+ indices.append(k)
+ elif np.abs(y - y_) < 1e-2 and self.y_min < y < self.y_max:
+ indices.append(k)
+ k = np.random.choice(indices)
+ (x, y), (x_, y_) = coords[k:k + 2]
+ is_vertical = np.abs(x - x_) < 1e-2
+ line = LineString(coords[k:k + 2])
+ mod = len(coords) - 1
+ stride = constants.UNIT * (np.random.randint(self.max_stride) + 1)
+ if is_vertical:
+ new_x = x + stride if (y_ < y) ^ out else x - stride
+ new_first = new_x, linear_extend_x(coords[(k - 1) % mod], coords[k], new_x)
+ new_second = new_x, linear_extend_x(coords[(k + 2) % mod], coords[k + 1], new_x)
+ else:
+ new_y = y + stride if (x_ > x) ^ out else y - stride
+ new_first = linear_extend_y(coords[(k - 1) % mod], coords[k], new_y), new_y
+ new_second = linear_extend_y(coords[(k + 2) % mod], coords[k + 1], new_y), new_y
+ coords[k % mod] = new_first
+ coords[(k + 1) % mod] = new_second
+ coords[-1] = coords[0]
+ s = canonicalize(Polygon(LineString(coords)))
+ return s, line, is_vertical
+
+ def extrude_room_out(self, assignment, info, i):
+ segments, shared_edges = map(info.get, ['segments', 'info'])
+ s, _, _ = self.extrude_room(i, info, True)
+ if not is_valid_polygon(s):
+ return RoomSolverMsg('extrude_room_out_invalid', [i])
+ cutter = s.difference(segments[i])
+ if not is_valid_polygon(cutter):
+ return RoomSolverMsg('extrude_room_out_invalid', [i])
+ cutter = canonicalize(cutter)
+ shared = list(k for k in info['shared_edges'][i].keys() if segments[k].intersection(cutter).area > .1)
+ index_changed = [i, *shared]
+ total_pre_area = sum([segments[i].area for i in index_changed])
+ for l in shared:
+ segments[l] = canonicalize(segments[l].difference(cutter))
+ segments[i] = s
+ total_post_area = sum([segments[i].area for i in index_changed])
+ if np.abs(total_pre_area - total_post_area) < .1:
+ return RoomSolverMsg('success', index_changed)
+ else:
+ return RoomSolverMsg('extrude_room_out_oob', index_changed)
+
+ def extrude_room_in(self, assignment, info, i):
+ segments, shared_edges = map(info.get, ['segments', 'shared_edges'])
+ s, line, is_vertical = self.extrude_room(i, info, False)
+ if not is_valid_polygon(s):
+ return RoomSolverMsg('extrude_room_in_invalid', [i])
+ cutter = segments[i].difference(s)
+ if not is_valid_polygon(cutter):
+ return RoomSolverMsg('extrude_room_in_invalid', [i])
+ cutter = canonicalize(cutter)
+ shared = {}
+ for k in shared_edges[i].keys():
+ with np.errstate(invalid="ignore"):
+ forward, backward = shared_paths(segments[k].boundary, line).geoms
+ if forward.length > 0:
+ shared[k] = forward.geoms[0]
+ elif backward.length > 0:
+ shared[k] = backward.geoms[0]
+ index_changed = [i, *shared.keys()]
+ ranges = []
+ for k, ls in shared.items():
+ if is_vertical:
+ y0, y1 = ls.xy[1]
+ ranges.append((min(y0, y1), max(y0, y1)))
+ else:
+ x0, x1 = ls.xy[0]
+ ranges.append((min(x0, x1), max(x0, x1)))
+ indices = np.argsort([(m + mm) / 2 for m, mm in ranges])
+ affected = [list(shared.keys())[_] for _ in indices]
+ cuts = [ranges[_][0] for _ in indices[1:]]
+ if is_vertical:
+ lss = [LineString([(-1, c), (100, c)]) for c in cuts]
+ else:
+ lss = [LineString([(c, -1), (c, 100)]) for c in cuts]
+ polygons = cut_polygon_by_line(cutter, *lss)
+ polygons.sort(key=lambda p: p.centroid.coords[0][1 if is_vertical else 0])
+ total_pre_area = sum([segments[i].area for i in index_changed])
+ for a, p in zip(affected, polygons):
+ segments[a] = canonicalize(union(segments[a], p))
+ segments[i] = s
+ total_post_area = sum([segments[i].area for i in index_changed])
+ if np.abs(total_pre_area - total_post_area) < .1:
+ return RoomSolverMsg('success', index_changed)
+ else:
+ return RoomSolverMsg('extrude_room_in_oob', index_changed)
+
+ def swap_room(self, assignment, info, i):
+ j = np.random.choice(list(info['neighbours_all'][i]))
+ j_ = assignment.index(j)
+ i_ = assignment.index(i)
+ assignment[i_], assignment[j_] = j, i
+ return RoomSolverMsg('success', [i, j])
+
+
+class BlueprintStaircaseSolver:
+ def __init__(self, contours, max_stride=1):
+ self.contours = contours
+ self.max_stride = max_stride
+ self.n_trials = 100
+
+ def perturb_solution(self, assignments, infos):
+ resp = self.move_staircase(infos)
+ if not resp.is_success:
+ return resp
+ for info in infos:
+ for k in info['segments']:
+ update_staircase_occupancies(info['segments'], info['staircase'], info['staircase_occupancies'],
+ k)
+ return resp
+
+ def move_staircase(self, infos):
+ staircase = infos[0]['staircase']
+ if staircase is None:
+ return RoomSolverMsg('success')
+ directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
+ for i in range(self.n_trials):
+ stride = constants.UNIT * (np.random.randint(self.max_stride) + 1)
+ x, y = directions[np.random.randint(4)]
+ coords = list((x_ + x * stride, y_ + y * stride) for x_, y_ in staircase.boundary.coords[:])
+ p = Polygon(LineString(coords))
+ if self.contours[-1].contains(p):
+ for info in infos:
+ info['staircase'] = p
+ return RoomSolverMsg('success')
+ else:
+ return RoomSolverMsg('invalid staircase')
diff --git a/infinigen/core/constraints/example_solver/room/types.py b/infinigen/core/constraints/example_solver/room/types.py
new file mode 100644
index 000000000..ec9eba04c
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/types.py
@@ -0,0 +1,80 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+from typing import List
+
+import networkx as nx
+
+
+class RoomType:
+ Kitchen = "kitchen"
+ Bedroom = 'bedroom'
+ LivingRoom = 'living-room'
+ Closet = 'closet'
+ Hallway = 'hallway'
+ Bathroom = 'bathroom'
+ Garage = 'garage'
+ Balcony = 'balcony'
+ DiningRoom = 'dining-room'
+ Utility = 'utility'
+ Staircase = 'staircase'
+
+
+def get_room_type(name):
+ return name.split('_')[0]
+
+
+def get_room_level(name):
+ return int(name.split('-')[-1])
+
+
+class RoomGraph:
+ def __init__(self, children: List[List[int]], rooms, entrance=None):
+ self.neighbours = [[] for _ in children]
+ for i, cs in enumerate(children):
+ for c in cs:
+ self.neighbours[i].append(c)
+ self.neighbours[c].append(i)
+ self.rooms = rooms
+ self.entrance = entrance
+
+ @property
+ def is_planar(self):
+ try:
+ nx.planar_layout(self.to_nx)
+ return True
+ except nx.NetworkXException:
+ return False
+
+ @property
+ def to_nx(self):
+ g = nx.Graph()
+ g.add_nodes_from(self.rooms)
+ for k in range(len(self.rooms)):
+ for l in self.neighbours[k]:
+ g.add_edge(self.rooms[k], self.rooms[l])
+ return g
+
+ @property
+ def cycle_basis(self):
+ return nx.cycle_basis(self.to_nx)
+
+ def __getitem__(self, item):
+ return [i for i, r in enumerate(self.rooms) if get_room_type(r) == item]
+
+ def __len__(self):
+ return len(self.rooms)
+
+ def __str__(self):
+ return {'neighbours': self.neighbours, 'rooms': self.rooms, 'entrance': self.entrance}
+
+
+def make_demo_tree():
+ children = [[1, 2], [], [3, 4], [5, 6], [7], [8, 9], [10, 11], [], [], [12], [], [13], [], [14], []]
+ rooms = ['hallway_0', 'closet_0', 'kitchen_0', 'dining-room_0', 'utility_0', 'hallway_1', 'living-room_0',
+ 'utility_1', 'bathroom_0', 'bedroom_0', 'balcony_0', 'bedroom_1', 'closet_1', 'bathroom_1', 'closet_2']
+ return RoomGraph(children, rooms, 0)
+
+
+DEMO_GRAPH = make_demo_tree()
diff --git a/infinigen/core/constraints/example_solver/room/utils.py b/infinigen/core/constraints/example_solver/room/utils.py
new file mode 100644
index 000000000..9353c9fb5
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/room/utils.py
@@ -0,0 +1,149 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Lingjie Mei: primary author
+# - Karhan Kayan: fix constants
+
+from collections import defaultdict
+
+import bpy
+import numpy as np
+import shapely
+from shapely import LineString, MultiLineString, Polygon, remove_repeated_points, simplify
+from shapely.ops import linemerge, orient, polygonize, shared_paths, unary_union
+
+import infinigen.core.constraints.example_solver.room.constants as constants
+from infinigen.assets.utils.decorate import write_co
+from infinigen.assets.utils.object import new_circle
+from infinigen.assets.utils.shapes import simplify_polygon
+from infinigen.core.util import blender as butil
+
+SIMPLIFY_THRESH = 1e-6
+ANGLE_SIMPLIFY_THRESH = .2
+WELD_THRESH = .01
+
+
+def is_valid_polygon(p):
+ if isinstance(p, Polygon) and p.area > 0 and p.is_valid:
+ if len(p.interiors) == 0:
+ return True
+ return False
+
+
+def canonicalize(p):
+ p = p.buffer(0)
+ try:
+ while True:
+ p_ = shapely.force_2d(simplify_polygon(p))
+ l = len(p.boundary.coords)
+ if p.area == 0:
+ raise NotImplementedError('Polygon empty.')
+ p = orient(p_)
+ coords = np.array(p.boundary.coords[:])
+ rounded = np.round(coords / constants.UNIT) * constants.UNIT
+ coords = np.where(np.all(np.abs(coords - rounded) < 1e-3, -1)[:, np.newaxis], rounded, coords)
+ diff = coords[1:] - coords[:-1]
+ diff = diff / (np.linalg.norm(diff, axis=-1, keepdims=True) + 1e-6)
+ product = (diff[[-1] + list(range(len(diff) - 1))] * diff).sum(-1)
+ valid_indices = list(range(len(coords) - 1))
+ invalid_indices = np.nonzero((product < -.8) | (product > 1 - 1e-6))[0].tolist()
+ if len(invalid_indices) > 0:
+ i = invalid_indices[len(invalid_indices) // 2]
+ valid_indices.remove(i)
+ p = shapely.Polygon(coords[valid_indices + [valid_indices[0]]])
+ if len(p.exterior.coords) == l:
+ break
+ if not is_valid_polygon(p):
+ raise NotImplementedError('Invalid polygon')
+ return p
+ except AttributeError:
+ raise NotImplementedError('Invalid multi polygon')
+
+
+def unit_cast(x, unit=None):
+ if unit is None:
+ unit = constants.UNIT
+ return int(x / unit) * unit
+
+
+def abs_distance(x, y):
+ z = [0] * 4
+ z[0 if y[0] > x[0] else 1] = np.abs(y[0] - x[0])
+ z[2 if y[1] > x[1] else 3] = np.abs(y[1] - x[1])
+ return np.array(z)
+
+
+def update_exterior_edges(segments, shared_edges, exterior_edges=None, i=None):
+ if exterior_edges is None: exterior_edges = {}
+ for k, s in segments.items():
+ if i is None or k == i:
+ l = s.boundary
+ for ls in shared_edges[k].values():
+ l = l.difference(ls)
+ if l.length > 0:
+ exterior_edges[k] = MultiLineString([l]) if isinstance(l, LineString) else l
+ elif k in exterior_edges:
+ exterior_edges.pop(k)
+ return exterior_edges
+
+
+def update_shared_edges(segments, shared_edges=None, i=None):
+ if shared_edges is None: shared_edges = defaultdict(dict)
+ for k, s in segments.items():
+ for l, t in segments.items():
+ if k != l and (i is None or k == i or l == i):
+ with np.errstate(invalid="ignore"):
+ forward, backward = shared_paths(s.boundary, t.boundary).geoms
+ if forward.length > 0:
+ shared_edges[k][l] = forward
+ elif backward.length > 0:
+ shared_edges[k][l] = backward
+ elif l in shared_edges[k]:
+ shared_edges[k].pop(l)
+ return shared_edges
+
+
+def update_staircase_occupancies(segments, staircase, staircase_occupancies=None, i=None):
+ if staircase is None: return None
+ if staircase_occupancies is None: staircase_occupancies = defaultdict(dict)
+ for k, s in segments.items():
+ if i is None or k == i:
+ staircase_occupancies[k] = s.intersection(staircase).area / staircase.area
+ return staircase_occupancies
+
+
+def compute_neighbours(ses, margin):
+ return list(l for l, se in ses.items() if any(ls.length >= margin for ls in se.geoms))
+
+
+def linear_extend_x(base, target, new_x):
+ return target[1] + (new_x - target[0]) * (base[1] - target[1]) / (base[0] - target[0])
+
+
+def linear_extend_y(base, target, new_y):
+ return target[0] + (new_y - target[1]) * (base[0] - target[0]) / (base[1] - target[1])
+
+
+def cut_polygon_by_line(polygon, *args):
+ merged = linemerge([polygon.boundary, *args])
+ borders = unary_union(merged)
+ polygons = polygonize(borders)
+ return list(polygons)
+
+
+def polygon2obj(p, reversed=False):
+ x, y = orient(p).exterior.xy
+ obj = new_circle(vertices=len(x) - 1)
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.edge_face_add()
+ if reversed:
+ bpy.ops.mesh.flip_normals()
+ write_co(obj, np.stack([x[:-1], y[:-1], np.zeros_like(x[:-1])], -1))
+ return obj
+
+
+def buffer(p, distance):
+ with np.errstate(invalid="ignore"):
+ return remove_repeated_points(simplify(p.buffer(distance, join_style='mitre'), SIMPLIFY_THRESH))
diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py
new file mode 100644
index 000000000..4a4c6e8ef
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/solve.py
@@ -0,0 +1,242 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import logging
+from pathlib import Path
+import copy
+
+import bpy
+import numpy as np
+from tqdm import trange, tqdm
+import gin
+from infinigen.core import surface
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ usage_lookup,
+ evaluator,
+ checks
+)
+from infinigen.core.constraints.example_solver.geometry import parse_scene, planes
+
+from .room import RoomSolver, MultistoryRoomSolver
+
+from infinigen.core.constraints.example_solver.state_def import State
+from infinigen.core.constraints.constraint_language.util import delete_obj
+
+from infinigen.core.constraints.example_solver import (
+ propose_continous,
+ propose_discrete,
+ greedy,
+)
+from infinigen.core.constraints.evaluator import domain_contains
+
+from infinigen.core.placement.placement import parse_asset_name
+from infinigen.core.util import blender as butil
+from infinigen.core import tagging, tags as t
+
+from .annealing import SimulatedAnnealingSolver
+
+logger = logging.getLogger(__name__)
+
+def map_range(x, xmin, xmax, ymin, ymax, exp=1):
+
+ if x < xmin:
+ return ymin
+ if x > xmax:
+ return ymax
+
+ t = (x - xmin) / (xmax - xmin)
+ return ymin + (ymax - ymin) * t ** exp
+
+@gin.register
+class LinearDecaySchedule:
+
+ def __init__(self, start, end, pct_duration):
+ self.start = start
+ self.end = end
+ self.pct_duration = pct_duration
+
+ def __call__(self, t):
+ return map_range(t, 0, self.pct_duration, self.start, self.end)
+
+@gin.configurable
+class Solver:
+
+ def __init__(
+ self,
+ output_folder: Path,
+ multistory: bool = False,
+ restrict_moves: list = None
+ ):
+
+ """ Initialize the solver
+
+ Parameters
+ ----------
+ output_folder : Path
+ The folder to save output plots to
+ print_report_freq : int
+ How often to print loss reports
+ multistory : bool
+ Whether to use the multistory room solver
+ constraints_greedy_unsatisfied : str | None
+ What do we do if relevant constraints are unsatisfied at the end of a greedy stage?
+ Options are 'warn` or `abort` or None
+
+ """
+
+ self.output_folder = output_folder
+
+ self.optim = SimulatedAnnealingSolver(
+ output_folder=output_folder,
+ )
+ self.room_solver_fn = MultistoryRoomSolver if multistory else RoomSolver
+ self.state: State = None
+ self.all_roomtypes = None
+ self.dimensions = None
+
+ self.moves = self._configure_move_weights(restrict_moves)
+
+
+ def _configure_move_weights(self, restrict_moves):
+
+ schedules = {
+ 'addition': (
+ propose_discrete.propose_addition,
+ LinearDecaySchedule(6, 0.1, 0.9),
+ ),
+ 'deletion': (
+ propose_discrete.propose_deletion,
+ LinearDecaySchedule(2, 0.0, 0.5),
+ ),
+ 'plane_change': (
+ propose_discrete.propose_relation_plane_change,
+ LinearDecaySchedule(2, 0.1, 1),
+ ),
+ 'resample_asset': (
+ propose_discrete.propose_resample,
+ LinearDecaySchedule(1, 0.1, 0.7),
+ ),
+ 'reinit_pose': (
+ propose_continous.propose_reinit_pose,
+ LinearDecaySchedule(1, 0.5, 1),
+ ),
+ 'translate': (
+ propose_continous.propose_translate,
+ 1
+ ),
+ 'rotate': (
+ propose_continous.propose_rotate,
+ 0.5
+ ),
+ }
+
+ if restrict_moves is not None:
+ schedules = {k: v for k, v in schedules.items() if k in restrict_moves}
+ logger.info(f'Restricting {self.__class__.__name__} moves to {list(schedules.keys())}')
+
+ return schedules
+
+ @gin.configurable
+ def choose_move_type(
+ self,
+ it: int,
+ max_it: int,
+ ):
+ t = it / max_it
+ names, confs = zip(*self.moves.items())
+ funcs, scheds = zip(*confs)
+ weights = np.array([s if isinstance(s, (float, int)) else s(t) for s in scheds])
+ return np.random.choice(funcs, p=weights/weights.sum())
+
+ def solve_rooms(self, scene_seed, consgraph: cl.Problem, filter: r.Domain):
+ self.state, self.all_roomtypes, self.dimensions = self.room_solver_fn(scene_seed).solve()
+ return self.state
+
+ @gin.configurable
+ def solve_objects(
+ self,
+ consgraph: cl.Problem,
+ filter_domain: r.Domain,
+ var_assignments: dict[str, str],
+ n_steps: int,
+ desc: str,
+ abort_unsatisfied: bool = False,
+ print_bounds: bool = False,
+ ):
+
+ filter_domain = copy.deepcopy(filter_domain)
+
+ desc_full = (desc, *var_assignments.values())
+
+ dom_assignments = {k: r.Domain(self.state.objs[objkey].tags) for k, objkey in var_assignments.items()}
+ filter_domain = r.substitute_all(filter_domain, dom_assignments)
+
+ if not r.domain_finalized(filter_domain):
+ raise ValueError(f'Cannot solve {desc_full=} with non-finalized domain {filter_domain}')
+
+ orig_bounds = r.constraint_bounds(consgraph)
+ bounds = propose_discrete.preproc_bounds(
+ orig_bounds,
+ self.state,
+ filter_domain,
+ print_bounds=print_bounds
+ )
+
+ if len(bounds) == 0:
+ logger.info(f'No objects to be added for {desc_full=}, skipping')
+ return self.state
+
+ active_count = greedy.update_active_flags(self.state, var_assignments)
+
+ n_start = len(self.state.objs)
+ logger.info(
+ f"Greedily solve {desc_full} - stage has {len(bounds)}/{len(orig_bounds)} bounds, "
+ f"{active_count=}/{len(self.state.objs)} objs"
+ )
+
+ self.optim.reset(max_iters=n_steps)
+ ra = trange(n_steps) if self.optim.print_report_freq == 0 else range(n_steps)
+ for j in ra:
+ move_gen = self.choose_move_type(j, n_steps)
+ self.optim.step(consgraph, self.state, move_gen, filter_domain)
+ self.optim.save_stats(self.output_folder/f'optim_{desc}.csv')
+
+ logger.info(
+ f"Finished solving {desc_full}, added {len(self.state.objs) - n_start} "
+ f"objects, loss={self.optim.curr_result.loss():.4f} viol={self.optim.curr_result.viol_count()}"
+ )
+
+ logger.info(self.optim.curr_result.to_df())
+
+ violations = {
+ k: v for k, v in self.optim.curr_result.violations.items()
+ if v > 0
+ }
+
+ if len(violations):
+ msg = f'Solver has failed to satisfy constraints for stage {desc_full}. {violations=}.'
+ if abort_unsatisfied:
+ butil.save_blend(self.output_folder/f'abort_{desc}.blend')
+ raise ValueError(msg)
+ else:
+ msg += ' Continuing anyway, override `solve_objects.abort_unsatisfied=True` via gin to crash instead.'
+ logger.warning(msg)
+
+ # re-enable everything so the blender scene populates / displays correctly etc
+ for k, v in self.state.objs.items():
+ greedy.set_active(self.state, k, True)
+
+ return self.state
+
+ def get_bpy_objects(self, domain: r.Domain) -> list[bpy.types.Object]:
+ objkeys = domain_contains.objkeys_in_dom(domain, self.state)
+ return [
+ self.state.objs[k].obj for k in objkeys
+ ]
+
diff --git a/infinigen/core/constraints/example_solver/state_def.py b/infinigen/core/constraints/example_solver/state_def.py
new file mode 100644
index 000000000..bc325d6d8
--- /dev/null
+++ b/infinigen/core/constraints/example_solver/state_def.py
@@ -0,0 +1,207 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Alexander Raistrick: state, print, to_json
+# - Karhan Kayan: add dof / trimesh
+
+from __future__ import annotations
+from dataclasses import dataclass, field
+import typing
+import pickle
+import copy
+import importlib
+import logging
+from collections import OrderedDict
+import json
+from pathlib import Path
+import enum
+from collections.abc import Collection
+
+import numpy as np
+import bpy
+import shapely
+import trimesh
+from infinigen.core.constraints.example_solver.geometry.planes import Planes
+
+from infinigen.core.placement.factory import AssetFactory
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r
+)
+from .geometry import parse_scene
+import trimesh
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+@dataclass
+class RelationState:
+ relation: cl.Relation
+ target_name: str
+ child_plane_idx: int = None
+ parent_plane_idx: int = None
+
+@dataclass
+class ObjectState:
+
+ obj: bpy.types.Object
+ generator: typing.Optional[AssetFactory] = None
+ tags: set = field(default_factory=set)
+ relations: list[RelationState] = field(default_factory=list)
+
+ dof_matrix_translation: np.array = None
+ dof_rotation_axis: np.array = None
+ contour : shapely.Geometry = None
+ _pose_affects_score = None
+
+ fcl_obj = None
+ col_obj = None
+
+ # store whether this object is active for the current greedy stage
+ # inactive objects arent returned by scene() and arent accessible through blender (for perf)
+ # updated by greedy.update_active_flags()
+ active: bool = True
+
+ def __post_init__(self):
+ assert not t.contradiction(self.tags)
+ assert not any(isinstance(r.relation, cl.NegatedRelation) for r in self.relations), self.relations
+
+ def __repr__(self):
+ obj = self.obj
+ tags = self.tags
+ relations = self.relations
+
+ name = obj.name if obj is not None else None
+ return f"{self.__class__.__name__}(obj.name={name}, {tags=}, {relations=})"
+
+@dataclass
+class State:
+
+ objs: OrderedDict[str, ObjectState]
+
+ trimesh_scene: trimesh.Scene = None
+ bvh_cache : dict = field(default_factory=dict)
+ planes: Planes = None
+
+ def print(self):
+
+ print(f"State ({len(self.objs)} objs)")
+ order = sorted(
+ self.objs.keys(),
+ key=lambda s: s.split('_')[-1]
+ )
+ for k in order:
+ v = self.objs[k]
+ relations = ', '.join(
+ f'{r.relation.__class__.__name__}({r.target_name})'
+ for r in v.relations
+ )
+ semantics = {tg for tg in t.decompose_tags(v.tags)[0] if not isinstance(tg, t.SpecificObject)}
+ print(f" {v.obj.name} {semantics} [{relations}]")
+
+ def to_json(self, path: Path):
+
+ JSON_SUPPORTED_TYPES = (
+ int, float, str, bool, list, dict
+ )
+
+ def preprocess_field(x):
+ match x:
+ case np.ndarray():
+ return x.tolist()
+ case np.int64():
+ return x.item()
+ case t.Tag():
+ return str(x)
+ case bpy.types.Object():
+ return x.name
+ case enum.Enum():
+ return x.name
+ case type():
+ return x.__module__ + '.' + x.__name__
+ case set() | frozenset():
+ return list(x)
+ case val if isinstance(val, JSON_SUPPORTED_TYPES):
+ return x
+ case AssetFactory():
+ return repr(x)
+ case ObjectState() | RelationState():
+ return x.__dict__
+ case cl.Relation():
+ res = x.__dict__
+ res['relation_type'] = x.__class__.__name__
+ return res
+ case _:
+ return ""
+
+ data = {
+ 'objs': self.objs,
+ }
+
+ with path.open('w') as f:
+ json.dump(
+ data,
+ f,
+ default=preprocess_field,
+ sort_keys=True,
+ indent=4,
+ check_circular=True
+ )
+
+ def __post_init__(self):
+ bpy_objs = [o.obj for o in self.objs.values() if o.obj is not None]
+ self.trimesh_scene = parse_scene.parse_scene(bpy_objs)
+ self.planes = Planes()
+
+ def save(self, filename: str):
+ return
+ # serialize objs and python modules
+ for os in self.objs.values():
+ os.obj = os.obj.name
+ if os.generator is not None:
+ path = os.generator.__module__ + '.' + os.generator.__name__
+ os.generator = path
+
+ with open(filename, 'wb') as file:
+ pickle.dump(self, file)
+
+ for os in self.objs.values():
+ os.obj = bpy.data.objects[os.obj]
+
+ if os.generator is not None:
+ *mod, name = os.generator.split('.')
+ mod = importlib.import_module('.'.join(mod))
+ os.generator = getattr(mod, name)
+
+ @classmethod
+ def load(cls, filename: str):
+ with open(filename, 'rb') as file:
+ state = pickle.load(file)
+
+ # all objs were serialized as strings, unpack them
+ for o in state.objs:
+ if o.obj not in bpy.data.objects:
+ raise ValueError(
+ f"While deserializing {filename}, found name {o.obj=} which "
+ "isnt present in current blend scene. Did you load the "
+ "correct blend before loading the state?"
+ )
+ o.obj = bpy.data.objects[o.obj]
+
+
+def state_from_dummy_scene(col: bpy.types.Collection) -> State:
+
+ objs = {}
+ for obj in col.all_objects:
+ obj.rotation_mode = 'AXIS_ANGLE'
+ tags = {t.Semantics(c.name) for c in col.children if obj.name in c.objects}
+ tags.add(t.SpecificObject(obj.name))
+ objs[obj.name] = ObjectState(
+ obj=obj,
+ generator=None,
+ tags=tags
+ )
+ return State(objs=objs)
+
diff --git a/infinigen/core/constraints/reasoning/__init__.py b/infinigen/core/constraints/reasoning/__init__.py
new file mode 100644
index 000000000..47a90d7c9
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/__init__.py
@@ -0,0 +1,22 @@
+from .constraint_bounding import Bound, constraint_bounds
+from .constraint_constancy import is_constant
+
+from .domain import (
+ reldom_implies,
+ reldom_compatible,
+ reldom_intersection,
+ reldom_intersects,
+ reldom_satisfies,
+
+ domain_finalized,
+)
+from .domain_substitute import (
+ domain_tag_substitute,
+ substitute_all,
+)
+from .constraint_domain import (
+ Domain,
+ constraint_domain,
+ FilterByDomain
+)
+from .expr_equal import expr_equal
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/constraint_bounding.py b/infinigen/core/constraints/reasoning/constraint_bounding.py
new file mode 100644
index 000000000..a283ae853
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/constraint_bounding.py
@@ -0,0 +1,210 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors:
+# - Alexander Raistrick: primary author
+# - David Yan: bounding for inequalities / expressions
+
+import operator
+import typing
+import copy
+import dataclasses
+from functools import partial
+import logging
+
+import numpy as np
+
+from infinigen.core.constraints import constraint_language as cl
+
+from .domain import Domain
+from .constraint_domain import constraint_domain
+from .domain_substitute import domain_tag_substitute
+
+from .constraint_constancy import is_constant
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+@dataclasses.dataclass
+class Bound:
+
+ domain: Domain = None
+ low: int = None
+ high: int = None
+
+ _init_ops: typing.ClassVar = {
+ operator.eq,
+ operator.le,
+ operator.ge,
+ operator.lt,
+ operator.gt,
+ }
+
+ @classmethod
+ def from_comparison(cls, opfunc, lhs, rhs):
+
+ lhs = lhs() if is_constant(lhs) else None
+ rhs = rhs() if is_constant(rhs) else None
+
+ if lhs is None == rhs is None:
+ raise ValueError(f'Attempted to create bound with neither side constant {lhs=} {rhs=}')
+ right_const = rhs is not None
+ val = rhs if right_const else lhs
+
+ match (opfunc, right_const):
+ case operator.eq, _:
+ return cls(low=val, high=val)
+ case (operator.le, False) | (operator.ge, True):
+ return cls(low=val)
+ case (operator.le, True) | (operator.ge, False):
+ return cls(high=val)
+ case (operator.lt, True):
+ return cls(high=val - 1)
+ case (operator.gt, False):
+ return cls(high=val - 1)
+ case (operator.lt, False):
+ return cls(low=val + 1)
+ case (operator.gt, True):
+ return cls(low=val + 1)
+ case _:
+ raise ValueError(f'Unhandled case {opfunc=}, {right_const=}')
+
+ def map(self, func, lhs=None, rhs=None):
+
+ if lhs is None == rhs is None:
+ raise ValueError(f'Expected exactly one of {lhs=} {rhs=} to be provided')
+
+ if lhs is not None:
+ return Bound(
+ low=func(lhs, self.low),
+ high=func(lhs, self.high)
+ )
+ else:
+ return Bound(
+ low=func(self.low, rhs),
+ high=func(self.high, rhs)
+ )
+
+int_inverse_op = {
+ operator.add: operator.sub,
+ operator.mul: operator.floordiv,
+}
+int_inverse_op.update({v: k for k, v in int_inverse_op.items()})
+
+def _expression_map_bound_binop(
+ node: cl.ScalarOperatorExpression,
+ bound: Bound
+) -> list[Bound]:
+
+ lhs, rhs = node.operands
+ inv_func = int_inverse_op.get(node.func)
+ if inv_func is None:
+ return []
+
+ consts = is_constant(lhs), is_constant(rhs)
+ match consts:
+ case (False, False):
+ return []
+ case (True, False):
+ return expression_map_bound(rhs, bound.map(inv_func, lhs=lhs()))
+ case (False, True):
+ return expression_map_bound(lhs, bound.map(inv_func, rhs=rhs()))
+ case (True, True): # both const, nothing to bound
+ return []
+ case _:
+ raise ValueError("Impossible")
+
+def evaluate_known_vars(node: cl.Node, known_vars) -> cl.constant:
+ if is_constant(node):
+ return None
+ match node:
+ case cl.ScalarOperatorExpression(f, (lhs, rhs)) if f in int_inverse_op.keys() or f in int_inverse_op:
+ if is_constant(lhs):
+ rhs_eval = evaluate_known_vars(rhs, known_vars)
+ if is_constant(rhs_eval): return f(lhs, rhs_eval)
+ else: return None
+ else:
+ lhs_eval = evaluate_known_vars(lhs, known_vars)
+ if is_constant(lhs_eval): return f(lhs_eval, rhs)
+ else: return None
+ case cl.count(objs):
+ return evaluate_known_vars(objs, known_vars)
+ case cl.ObjectSetExpression() as objs:
+ domain = constraint_domain(objs)
+ vals = []
+ for known_domain, known_val in known_vars:
+ if domain == known_domain:
+ vals.append(known_val)
+ if len(vals) == 0:
+ return None
+ else:
+ return cl.constant(min(vals))
+ case _:
+ raise NotImplementedError(node)
+
+def expression_map_bound(node: cl.Node, bound: Bound) -> list[Bound]:
+
+ match node:
+ case cl.ScalarOperatorExpression(f, (lhs, rhs)) if f in int_inverse_op.keys():
+ return _expression_map_bound_binop(node, bound)
+ case cl.count(objs):
+ return expression_map_bound(objs, bound)
+ case cl.ObjectSetExpression() as objs:
+ bound = Bound(
+ domain=constraint_domain(objs),
+ low=bound.low,
+ high=bound.high,
+ )
+ return [bound]
+ case _:
+ # distance & other hard constraints do not produce quantity-bounds
+ return []
+
+def update_var(var, scene_state):
+ if not is_constant(var) and not isinstance(var, int) and scene_state is not None:
+ var_eval = evaluate_known_vars(var, scene_state)
+ var = var_eval if is_constant(var_eval) else var
+ return var
+
+def constraint_bounds(
+ node: cl.Node,
+ state=None
+) -> list[Bound]:
+
+ recurse = partial(constraint_bounds, state=state)
+
+ match node:
+ case cl.Problem(cons):
+ return sum((recurse(c) for c in cons.values()), [])
+ case cl.BoolOperatorExpression(operator.and_, cons):
+ return sum((recurse(c) for c in cons), [])
+ case cl.in_range(val, low, high):
+ low = update_var(low, state)
+ high = update_var(high, state)
+ if is_constant(low) and is_constant(high):
+ low = low()
+ high = high()
+ bound = Bound(low=low, high=high)
+ return expression_map_bound(val, bound)
+ case cl.BoolOperatorExpression(f, (lhs, rhs)) if f in Bound._init_ops:
+ lhs, rhs = update_var(lhs, state), update_var(rhs, state)
+
+ if not is_constant(lhs) and not is_constant(rhs):
+ logger.debug(f'Encountered {cl.BoolOperatorExpression.__name__} {f} with non-constant lhs and rhs. Producing no bound.')
+ return []
+
+ bound = Bound.from_comparison(node.func, lhs, rhs)
+ expr = rhs if is_constant(lhs) else lhs
+ return expression_map_bound(expr, bound)
+ case cl.ForAll(objs, varname, pred):
+ o_domain = constraint_domain(objs)
+ bounds = recurse(pred)
+ for b in bounds:
+ # TODO INCORRECT. Doesnt force EVERY object in o_domain to satify the bound
+ b.domain = domain_tag_substitute(b.domain, t.Variable(varname), o_domain)
+ return bounds
+ case unmatched:
+ assert isinstance(unmatched, cl.Expression), unmatched
+ return []
+
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/constraint_constancy.py b/infinigen/core/constraints/reasoning/constraint_constancy.py
new file mode 100644
index 000000000..54523b7ca
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/constraint_constancy.py
@@ -0,0 +1,21 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import operator
+import typing
+
+import numpy as np
+
+from infinigen.core.constraints import constraint_language as cl
+
+def is_constant(node: cl.Node):
+ match node:
+ case cl.constant():
+ return True
+ case cl.BoolOperatorExpression(_, vs) | cl.ScalarOperatorExpression(_, vs):
+ return all(is_constant(x) for x in vs)
+ case _:
+ return False
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/constraint_domain.py b/infinigen/core/constraints/reasoning/constraint_domain.py
new file mode 100644
index 000000000..e627a3c56
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/constraint_domain.py
@@ -0,0 +1,75 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from __future__ import annotations
+import logging
+import itertools
+from functools import partial
+
+from dataclasses import dataclass, field
+import copy
+import typing
+
+import numpy as np
+
+from infinigen.core.constraints import constraint_language as cl
+from infinigen.core import tags as t
+from .domain import Domain
+
+logger = logging.getLogger(__name__)
+
+def constraint_domain(
+ node: cl.ObjectSetExpression,
+ finalize_variables=False
+) -> Domain:
+
+ """Given an expression, find a compact representation of what types of objects it is applying to.
+
+ User can compared the resulting Domain against their State and see what objects fit.
+ """
+
+ assert isinstance(node, cl.ObjectSetExpression), node
+
+ recurse = partial(constraint_domain, finalize_variables=finalize_variables)
+
+ match node:
+ case cl.tagged(objs, tags):
+ d = recurse(objs)
+ d.tags.update(tags)
+ if t.contradiction(d.tags):
+ raise ValueError(f'Contradictory tags {tags=} for {d=} while parsing constraint {node=}')
+ return d
+ case cl.related_to(children, parents, relation):
+ c_d = recurse(children)
+ p_d = recurse(parents)
+ c_d.add_relation(relation, p_d)
+ return c_d
+ case cl.scene():
+ return Domain()
+ case cl.item(x):
+ if finalize_variables:
+ return recurse(node.member_of) # TODO - worried about infinite recursion somehow
+ else:
+ return Domain(tags={t.Variable(x)})
+ case FilterByDomain(objs, filter):
+ return filter.intersection(recurse(objs))
+ case _:
+ raise NotImplementedError(node)
+
+@dataclass
+class FilterByDomain(cl.ObjectSetExpression):
+
+ """ Constraint node which says to return all objects matching a domain.
+
+ Used as a compacted representation of the filtering performed many cl.tagged and cl.related_to calls.
+ One r.Domain is sufficient to represent the effect of and combination of intersection-style filtering.
+
+ Introduced (currently) only by greedy.filter_constraints, since that function needs to work
+ with domains in order to narrow the scope of some constraints.
+ """
+
+ objs: cl.ObjectSetExpression
+ filter: Domain
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/domain.py b/infinigen/core/constraints/reasoning/domain.py
new file mode 100644
index 000000000..f36815c23
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/domain.py
@@ -0,0 +1,416 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from __future__ import annotations
+import logging
+import itertools
+
+from dataclasses import dataclass, field
+import copy
+import typing
+
+import numpy as np
+
+from infinigen.core.constraints import constraint_language as cl
+from infinigen.core import tags as t
+
+logger = logging.getLogger(__name__)
+
+def reldom_implies(
+ a: tuple[cl.Relation, Domain],
+ b: tuple[cl.Relation, Domain]
+):
+ """ If relation a is satisfied, is relation guaranteed to be satisfied?
+ """
+
+ assert isinstance(a[1], Domain)
+ assert isinstance(b[1], Domain)
+
+ return (
+ a[0].implies(b[0]) and
+ a[1].implies(b[1])
+ )
+
+def reldom_compatible(
+ a: tuple[cl.Relation, Domain],
+ b: tuple[cl.Relation, Domain],
+):
+ """ If relation a is satisfied, can relation b be satisfied?
+ """
+
+ assert isinstance(a[1], Domain)
+ assert isinstance(b[1], Domain)
+
+ a_neg = isinstance(a[0], cl.NegatedRelation)
+ b_neg = isinstance(b[0], cl.NegatedRelation)
+ match (a_neg, b_neg):
+ case True, False:
+ if b[0].implies(a[0].rel) and b[1].intersects(a[1]):
+ logger.debug('reldom_compatible found contradicting negated %s %s', a[0], b[0])
+ return False
+ case False, True:
+ if a[0].implies(b[0].rel) and a[1].intersects(b[1]):
+ logger.debug('reldom_compatible found contradicting negated %s %s', a[0], b[0])
+ return False
+
+ return True
+
+def reldom_satisfies(
+ a: tuple[cl.Relation, Domain],
+ b: tuple[cl.Relation, Domain],
+):
+ return (
+ a[0].intersects(b[0], strict=True)
+ and a[1].satisfies(b[1])
+ )
+
+def reldom_intersects(
+ a: tuple[cl.Relation, Domain],
+ b: tuple[cl.Relation, Domain],
+ **kwargs
+):
+ return (
+ a[0].intersects(b[0]) and
+ a[1].intersects(b[1], **kwargs)
+ )
+
+
+def reldom_intersection(
+ a: tuple[cl.Relation, Domain],
+ b: tuple[cl.Relation, Domain],
+):
+ return (
+ a[0].intersection(b[0]),
+ a[1].intersection(b[1])
+ )
+
+
+def domain_finalized(dom: Domain, check_anyrel=False, check_variable=True):
+
+ if check_variable and any(isinstance(x, t.Variable) for x in dom.tags):
+ return False
+
+ for rel, cdom in dom.relations:
+ if check_anyrel and isinstance(rel, cl.AnyRelation):
+ return False
+ if not domain_finalized(cdom):
+ return False
+
+ return True
+
+@dataclass
+class Domain:
+
+ '''
+ Describes a class of object in the scene
+
+ Objects are in the domain if:
+ - Some part of the object is tagged with each of 'tags'
+ - It is related to an object matching each Domain in relations
+
+ WARNING: Recurive datastructure, here be dragons
+
+ Note: Default-constructed Domain contains Everything
+ '''
+
+ tags: set[t.Semantics] = field(default_factory=set)
+ relations: list[tuple[cl.Relation, Domain]] = field(default_factory=list)
+
+ def repr(self, abbrv=False, onelevel=False, oneline=False):
+
+ is_neg = lambda x: isinstance(x, t.Negated)
+ def setrepr(s):
+ inner = ", ".join(
+ repr(x) for x in sorted(list(s), key=is_neg)
+ if not (abbrv and isinstance(x, t.Negated))
+ )
+ return '{' + inner + '}'
+
+ next_abbrv = abbrv or onelevel
+ def repr_reldom(r, d):
+ if abbrv:
+ rel = f'-{r.rel.__class__.__name__}' if isinstance(r, cl.NegatedRelation) else f'{r.__class__.__name__}'
+ return f'({rel}(...), Domain({setrepr(d.tags)}, [...]))'
+ else:
+ return f'({repr(r)}, {d.repr(abbrv=next_abbrv)})'
+
+ relations = [
+ repr_reldom(r, d)
+ for r, d in sorted(
+ self.relations,
+ key=lambda x: isinstance(x[0], cl.NegatedRelation)
+ )
+ ]
+
+ if not oneline and sum(len(x) for x in relations) > 20:
+ relations = [r.replace('\n', '\n\t') for r in relations]
+ relations = '\n\t' + ',\n\t'.join(relations) + '\n'
+ else:
+ relations = ', '.join(relations)
+ return f'{self.__class__.__name__}({setrepr(self.tags)}, [{relations}])'
+
+ __repr__ = repr
+
+ def __post_init__(self):
+ assert isinstance(self.tags, set)
+ assert isinstance(self.relations, list)
+
+ def implies(self, other: Domain):
+
+ return (
+ t.implies(self.tags, other.tags)
+ and all(
+ any(reldom_implies(rel, orel) for rel in self.relations)
+ for orel in other.relations
+ )
+ )
+
+ def add_relation(
+ self,
+ new_rel: cl.Relation,
+ new_dom: Domain,
+ optimize_check_implies=True
+ ):
+
+ """
+ new_rel, new_dom: the relation and domain to be added
+ optimize_check_implies: bool
+ If enabled, dont add relations (aka predicates) that are already
+ provably true based on existing predicates. This should not be necessary for correctness:
+ object addition should check if relation constraints are satisfied/implied before adding more.
+ But it may (?) help speed, and the unit tests assume it is enabled for the most part
+ """
+
+ assert new_dom is not self
+
+ logger.debug('add_relation %s %s to existing %i', new_rel, new_dom, len(self.relations))
+
+ if not optimize_check_implies:
+ self.relations.append((new_rel, new_dom))
+ return
+
+ covered = False
+
+ for i, (er, ed) in enumerate(self.relations):
+ if isinstance(new_rel, cl.NegatedRelation):
+ continue
+ elif isinstance(er, cl.NegatedRelation):
+ continue
+ elif reldom_implies((er, ed), (new_rel, new_dom)):
+ covered = True
+ elif (
+ reldom_satisfies((er, ed), (new_rel, new_dom))
+ or reldom_satisfies((new_rel, new_dom), (er, ed))
+ ):
+ logger.debug('Tightening existing relation %s with %s', (er, ed), (new_rel, new_dom))
+ self.relations[i] = reldom_intersection((new_rel, new_dom), (er, ed))
+ covered = True
+ elif new_dom.intersects(ed, require_satisfies_right=True):
+ logger.debug('Tightening domain %s with %s', ed, new_dom)
+ self.relations[i] = (er, ed.intersection(new_dom))
+ else:
+ logger.debug('%s is not relevant for %s', (er, ed), (new_rel, new_dom))
+
+ if not covered:
+ logger.debug('optimize_check_implies found nothing, adding relation %s %s', new_rel, new_dom)
+ self.relations.append((new_rel, new_dom))
+
+ if self.is_recursive():
+ raise ValueError(f'Encountered recursive domain after add_relation {new_rel=} {new_dom=} onto {self.tags=} {len(self.relations)=}')
+
+ def with_relation(self, rel: cl.Relation, dom: Domain):
+ new = copy.deepcopy(self)
+ new.add_relation(rel, dom)
+ return new
+
+ def with_tags(self, tags: set[t.Semantics]):
+ if not isinstance(tags, set):
+ tags = {tags}
+ new = copy.deepcopy(self)
+ new.tags.update(tags)
+ return new
+
+ def satisfies(
+ self,
+ other: Domain
+ ):
+
+ """
+
+ Assumes that 'self' is fully specified: any predicates that arent listed are false.
+
+ Different from 'implies' in that if `other` contains negative predicates, `self` need not imply these,
+ it just needs to not contradict them.
+
+ Different from 'intersects' in that
+ """
+
+ logger.debug("%s for %s %s", Domain.satisfies.__name__, self, other)
+
+ if not t.satisfies(self.tags, other.tags):
+ logger.debug('failed tag implication %s -> %s', self.tags, other.tags)
+ return False
+
+ def bothsat(reldom1, reldom2):
+ return (
+ reldom1[0].satisfies(reldom2[0])
+ and reldom1[1].satisfies(reldom2[1])
+ )
+
+ for orel in other.relations:
+ match orel:
+ case (cl.NegatedRelation(n), d):
+
+ contradictor = next((
+ srel for srel in self.relations if bothsat(srel, (n, d))
+ ), None)
+
+ if contradictor is not None:
+ logger.debug(
+ 'satisfies found %s in self, which contradicts %s because it satisfies %s', contradictor, orel, (n, d)
+ )
+ return False
+ case _:
+ if not any(bothsat(srel, orel) for srel in self.relations):
+ logger.debug('found unsatisfied %s for %s', orel, self.relations)
+ return False
+
+ return True
+
+ def intersects(
+ self,
+ other: Domain,
+ require_satisfies_left=False,
+ require_satisfies_right=False
+ ):
+
+ """Return True if self and other could have a non-empty intersection.
+
+ Parameters
+ ----------
+ self: Domain - the domain to check
+ other: Domain - the domain to check against
+
+ require_satisfies_left: bool -
+ If True, assume that `self` is exhaustively specified (ie, any predicates not listed are false),
+ and therefore `other` must imply `self` for the intersection to be non-empty.
+ require_satisfies_right: bool -
+ If True, assume that `other` is exhaustively specified (ie, any predicates not listed are false),
+ and therefore `self` must imply `other` for the intersection to be non-empty.
+ """
+
+ logger.debug('Domain.intersects for \n\t%s \n\t%s', self, other)
+
+ if t.contradiction(self.tags.union(other.tags)):
+ logger.debug('tag contradiction %s, %s', self.tags, other.tags)
+ return False
+
+ # no relations can contradict eachother
+ for ard, brd in itertools.product(self.relations, other.relations):
+ if ard is brd:
+ continue
+ if not reldom_compatible(ard, brd):
+ logger.debug('found incompatible %s %s', ard, brd)
+ return False
+
+ # any relations actually known to be present must intersect
+ a_pos = [rd for rd in self.relations if not isinstance(rd[0], cl.NegatedRelation)]
+ b_pos = [rd for rd in other.relations if not isinstance(rd[0], cl.NegatedRelation)]
+ if require_satisfies_left:
+ if not t.satisfies(other.tags, self.tags):
+ return False
+ for ard in a_pos:
+ if not any(reldom_intersects(ard, brd) for brd in b_pos):
+ logger.debug('require_satisfies_left found no intersecting %s %s', ard, b_pos)
+ return False
+ if require_satisfies_right:
+ if not t.satisfies(self.tags, other.tags):
+ return False
+ for brd in b_pos:
+ if not any(reldom_intersects(ard, brd) for ard in a_pos):
+ logger.debug('require_satisfies_right found no intersecting %s %s', brd, a_pos)
+ return False
+
+ logger.debug('Domain.intersects for %s %s returning True', self, other)
+
+ return True
+
+ def intersection(self, other: Domain):
+
+ '''
+ Return a domain representing the intersection of self and other.
+ Result is at least as strict as self and other.
+
+ contains(self, x) and contains(other, x) -> contains(intersection, x)
+
+ TODO:
+ - does order relations are checked for intersection matter?
+ - almost certainly yes, intersection is not transitive.
+ - so what order is best? fewest remaining relations? does it matter?
+ '''
+
+ newtags = self.tags.union(other.tags)
+ if t.contradiction(newtags):
+ raise ValueError(f'Contradictory {newtags=} for {self.intersection} {other=}')
+
+ newdom = Domain(newtags)
+ for orel, odom in *self.relations, *other.relations:
+ newdom.add_relation(orel, copy.deepcopy(odom))
+
+ return newdom
+
+ def is_recursive(self, seen=None):
+
+ """ Check if this domain somehow references itself via its own relations.
+ Domains should ideally never reach this state; this function is used to check that they dont.
+ """
+
+ if seen is None:
+ seen = set()
+
+ if id(self) in seen:
+ return True
+
+ seen.add(id(self))
+
+ return any(
+ d.is_recursive(seen=seen)
+ for _, d in self.relations
+ )
+
+ def positive_part(self):
+ return Domain(
+ tags={ti for ti in self.tags if not isinstance(ti, t.Negated)},
+ relations=[
+ (r, d.positive_part())
+ for r, d in self.relations
+ if not isinstance(r, cl.NegatedRelation)
+ ]
+ )
+
+ def traverse(self):
+ yield self
+ for rel, dom in self.relations:
+ yield from dom.traverse()
+
+ def all_vartags(self) -> set[t.Variable]:
+ return {
+ x
+ for d in self.traverse()
+ for x in d.tags
+ if isinstance(x, t.Variable)
+ }
+
+ def get_objs_named(self):
+ objnames = {
+ x.name for x in self.tags
+ if isinstance(x, t.SpecificObject)
+ }
+ for rel, dom in self.relations:
+ if isinstance(rel, cl.NegatedRelation):
+ continue
+ objnames = objnames.union(dom.get_objs_named())
+ return objnames
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/domain_substitute.py b/infinigen/core/constraints/reasoning/domain_substitute.py
new file mode 100644
index 000000000..752fea0b6
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/domain_substitute.py
@@ -0,0 +1,66 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from __future__ import annotations
+import logging
+import itertools
+
+from dataclasses import dataclass, field
+import copy
+import typing
+
+import numpy as np
+
+from infinigen.core.constraints import constraint_language as cl
+from infinigen.core import tags as t
+from .constraint_domain import Domain
+
+logger = logging.getLogger(__name__)
+
+def domain_tag_substitute(
+ domain: Domain,
+ vartag: t.Variable,
+ subst_domain: Domain,
+ return_match=False
+) -> Domain:
+
+ """Return concrete substitution of `domain`, where `subst_domain` must be satisfied
+ whenever `subst_tag` was present in the original.
+ """
+
+ assert isinstance(vartag, t.Variable), vartag
+ domain = copy.deepcopy(domain) # prevent modification of original
+
+ o_match = vartag in domain.tags
+
+ rd_sub, rd_matches = [], []
+ for r, d in domain.relations:
+ d, match = domain_tag_substitute(d, vartag, subst_domain, return_match=True)
+ rd_sub.append((r, d))
+ rd_matches.append(match)
+ rd_match = any(rd_matches)
+
+ if not (o_match or rd_match):
+ return (domain, False) if return_match else domain
+
+ domain.relations = []
+ for r, d in rd_sub:
+ domain.add_relation(r, d)
+
+ if o_match:
+ if vartag in domain.tags:
+ domain.tags.remove(vartag)
+ domain = domain.intersection(subst_domain)
+
+ return (domain, True) if return_match else domain
+
+def substitute_all(
+ dom: Domain,
+ assignments: dict[t.Variable, Domain],
+) -> Domain:
+ for var, d in assignments.items():
+ dom = domain_tag_substitute(dom, var, d)
+ return dom
\ No newline at end of file
diff --git a/infinigen/core/constraints/reasoning/expr_equal.py b/infinigen/core/constraints/reasoning/expr_equal.py
new file mode 100644
index 000000000..9fd4a2a35
--- /dev/null
+++ b/infinigen/core/constraints/reasoning/expr_equal.py
@@ -0,0 +1,64 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import dataclasses
+
+from ..constraint_language.types import Node
+
+@dataclasses.dataclass
+class FalseEqualityResult:
+ n1: Node
+ n2: Node
+ reason: str
+
+ def __repr__(self) -> str:
+ # default dataclass repr is too long
+ c1 = self.n1.__class__.__name__
+ c2 = self.n2.__class__.__name__
+ return f'{self.__class__.__name__}({c1}, {c2}, {repr(self.reason)})'
+
+ def __bool__(self):
+ return False
+
+def expr_equal(n1: Node, n2: Node, name: str = None) -> bool | FalseEqualityResult:
+
+ """ An equality comparison operator for constraint Node expressions
+
+ Using the default Node == Node is unsafe since Nodes override ==
+ in order to return another expression
+ """
+
+ if not dataclasses.is_dataclass(n1) or not dataclasses.is_dataclass(n2):
+ raise ValueError(
+ f'expr_equal {name=} called with non-dataclass {n1.__class__=} {n2.__class__=}.'
+ ' Expected all Node types to be dataclasses'
+ )
+
+ if name is None:
+ name = n1.__class__.__name__
+
+ if type(n1) is not type(n2):
+ return FalseEqualityResult(n1, n2, f"Unequal types for {name}: {type(n1).__name__} != {type(n2).__name__}")
+
+ n1_child_keys = [k for k, _ in n1.children()]
+ n2_child_keys = [k for k, _ in n1.children()]
+ n1_children = [v for _, v in n1.children()]
+ n2_children = [v for _, v in n1.children()]
+
+ if n1_child_keys != n2_child_keys:
+ return FalseEqualityResult(n1, n2, f'Unequal child keys for {name}: {n1_children}!={n2_children}')
+
+ for f in dataclasses.fields(n1):
+ v1 = getattr(n1, f.name)
+ v2 = getattr(n2, f.name)
+ if isinstance(v1, Node):
+ res = expr_equal(v1, v2, name=f'{name}.{f.name}')
+ if not res:
+ return res
+ elif v1 != v2:
+ return FalseEqualityResult(n1, n2, f'Unequal attr {repr(f.name)}, {v1} != {v2}')
+
+ return True
\ No newline at end of file
diff --git a/infinigen/core/constraints/usage_lookup.py b/infinigen/core/constraints/usage_lookup.py
new file mode 100644
index 000000000..f2c0a83b1
--- /dev/null
+++ b/infinigen/core/constraints/usage_lookup.py
@@ -0,0 +1,48 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from collections import defaultdict
+from infinigen.core import tags as t
+
+_factory_lookup: dict[type, set[t.Tag]] = None
+_tag_lookup: dict[t.Tag, set[type]] = None
+
+def initialize_from_dict(d):
+
+ global _factory_lookup, _tag_lookup
+ _factory_lookup = defaultdict(set)
+ _tag_lookup = defaultdict(set)
+
+ for tag, fac_list in d.items():
+ _tag_lookup[tag] = set()
+ for fac in fac_list:
+ _factory_lookup[fac].add(tag)
+ _tag_lookup[tag].add(fac)
+
+def usages_of_factory(fac) -> set[t.Tag]:
+ return _factory_lookup[fac].union({t.FromGenerator(fac)})
+
+def factories_for_usage(tags: set[t.Tag]):
+ if not isinstance(tags, set):
+ tags = [tags]
+ else:
+ tags = list(tags)
+
+ res = _tag_lookup[tags[0]]
+ for t in tags[1:]:
+ res.intersection_update(_tag_lookup[t])
+ return res
+
+def all_usage_tags():
+ return _tag_lookup.keys()
+
+def all_factories():
+ return _factory_lookup.keys()
+
+def has_usage(fac, tag):
+ assert fac in _factory_lookup.keys(), fac
+ assert tag in _tag_lookup.keys(), tag
+ return tag in _factory_lookup[fac]
\ No newline at end of file
diff --git a/infinigen/core/execute_tasks.py b/infinigen/core/execute_tasks.py
index dee5ebcc7..0b082aaa9 100644
--- a/infinigen/core/execute_tasks.py
+++ b/infinigen/core/execute_tasks.py
@@ -14,6 +14,7 @@
import pprint
import time
from collections import defaultdict
+import pickle
# ruff: noqa: F402
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1" # This must be done BEFORE import cv2.
@@ -80,12 +81,13 @@
pipeline,
exporting
)
-
+from infinigen.tools.export import export_scene
from infinigen.core.util.math import FixedSeed, int_hash
from infinigen.core.util.logging import Timer, save_polycounts, create_text_file
from infinigen.core.util.pipeline import RandomStageExecutor
from infinigen.core.util.random import sample_registry
-from infinigen.assets.utils.tag import tag_system
+from infinigen.core.tagging import tag_system
+
logger = logging.getLogger(__name__)
@@ -226,12 +228,29 @@ def render(scene_seed, output_folder, camera_id, render_image_func=render_image,
with Timer('Render Frames'):
render_image_func(frames_folder=Path(output_folder), camera_id=camera_id)
+def triangulate_meshes():
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'MESH':
+ view_state = obj.hide_viewport
+ obj.hide_viewport = False
+ bpy.context.view_layer.objects.active = obj
+ obj.select_set(True)
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='SELECT')
+ logging.info(f"Triangulating {obj}")
+ bpy.ops.mesh.quads_convert_to_tris()
+ bpy.ops.object.mode_set(mode='OBJECT')
+ obj.select_set(False)
+ obj.hide_viewport = view_state
+
@gin.configurable
def save_meshes(scene_seed, output_folder, frame_range, resample_idx=False):
-
+
if resample_idx is not None and resample_idx > 0:
resample_scene(int_hash((scene_seed, resample_idx)))
-
+
+ triangulate_meshes()
+
for obj in bpy.data.objects:
obj.hide_viewport = obj.hide_render
@@ -285,9 +304,8 @@ def execute_tasks(
generate_resolution=(1280,720),
fps=24,
reset_assets=True,
- focal_length=None,
dryrun=False,
- optimize_terrain_diskusage=False,
+ optimize_terrain_diskusage=False
):
if input_folder != output_folder:
if reset_assets:
@@ -327,19 +345,26 @@ def execute_tasks(
if Task.Coarse in task:
butil.clear_scene(targets=[bpy.data.objects])
butil.spawn_empty(f'{infinigen.__version__=}')
- compose_scene_func(output_folder, scene_seed)
-
+ info = compose_scene_func(output_folder, scene_seed)
+ outpath = output_folder/"assets"
+ outpath.mkdir(exist_ok=True)
+ with open(outpath/"info.pickle", 'wb') as f:
+ pickle.dump(info, f, protocol=pickle.HIGHEST_PROTOCOL)
+
camera = cam_util.set_active_camera(*camera_id)
- if focal_length is not None:
- camera.data.lens = focal_length
group_collections()
if Task.Populate in task:
populate_scene(output_folder, scene_seed)
- if Task.FineTerrain in task:
- terrain = Terrain(scene_seed, surface.registry, task=task, on_the_fly_asset_folder=output_folder/"assets")
+ need_terrain_processing = 'OpaqueTerrain' in bpy.data.objects
+
+ if Task.FineTerrain in task and need_terrain_processing:
+ with open(output_folder/"assets"/"info.pickle", 'rb') as f:
+ info = pickle.load(f)
+ terrain = Terrain(scene_seed, surface.registry, task=task, on_the_fly_asset_folder=output_folder/"assets", height_offset=info["height_offset"], whole_bbox=info["whole_bbox"])
+
cameras = [cam_util.get_camera(i, j) for i, j in cam_util.get_cameras_ids()]
terrain.fine_terrain(output_folder, cameras=cameras, optimize_terrain_diskusage=optimize_terrain_diskusage)
@@ -369,7 +394,11 @@ def execute_tasks(
for col in bpy.data.collections['unique_assets'].children:
col.hide_viewport = False
- if Task.Render in task or Task.GroundTruth in task or Task.MeshSave in task:
+ if need_terrain_processing and (
+ Task.Render in task
+ or Task.GroundTruth in task
+ or Task.MeshSave in task
+ ):
terrain = Terrain(
scene_seed,
surface.registry,
@@ -386,6 +415,9 @@ def execute_tasks(
camera_id=camera_id,
resample_idx=resample_idx
)
+
+ if Task.Export in task:
+ export_scene(input_folder / output_blend_name, output_folder)
if Task.MeshSave in task:
save_meshes(
diff --git a/infinigen/core/init.py b/infinigen/core/init.py
index 3398f94d9..c8aff5c3c 100644
--- a/infinigen/core/init.py
+++ b/infinigen/core/init.py
@@ -1,5 +1,7 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
import bpy
import argparse
@@ -117,6 +119,7 @@ def apply_gin_configs(
configs: list[str] = None,
overrides: list[str] = None,
skip_unknown: bool = False,
+ finalize_config=False,
mandatory_folders: list[Path] = None,
mutually_exclusive_folders: list[Path] = None
):
@@ -192,11 +195,12 @@ def find_config(p):
f'At most one config file must be loaded from {mutex_folder} to avoid unexpected behavior, instead got {both=}'
)
- with LogLevel(logger=logging.getLogger(), level=logging.CRITICAL):
+ with LogLevel(logger=logging.getLogger(), level=logging.WARNING):
gin.parse_config_files_and_bindings(
configs,
- bindings=overrides,
- skip_unknown=skip_unknown
+ bindings=overrides,
+ skip_unknown=skip_unknown,
+ finalize_config=finalize_config
)
def import_addons(names):
diff --git a/infinigen/core/nodes/compatibility.py b/infinigen/core/nodes/compatibility.py
index 2441e1393..ff52e73e9 100644
--- a/infinigen/core/nodes/compatibility.py
+++ b/infinigen/core/nodes/compatibility.py
@@ -1,3 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick, David Yan
+
import logging
from collections import OrderedDict
diff --git a/infinigen/core/nodes/node_info.py b/infinigen/core/nodes/node_info.py
index afa52c001..4d66073ad 100644
--- a/infinigen/core/nodes/node_info.py
+++ b/infinigen/core/nodes/node_info.py
@@ -1,14 +1,11 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-
-# Authors: all infinigen authors
-
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
import bpy
import numpy as np
-
class Nodes:
"""
An enum for all node types.
@@ -24,7 +21,7 @@ class Nodes:
Attribute = "ShaderNodeAttribute"
CaptureAttribute = "GeometryNodeCaptureAttribute"
AttributeStatistic = 'GeometryNodeAttributeStatistic'
- TransferAttribute = "GeometryNodeAttributeTransfer" # removed in b3.4, still supported via compatibility.py
+ TransferAttribute = "GeometryNodeAttributeTransfer" # removed in b3.4, still supported via compatibility.py
DomainSize = 'GeometryNodeAttributeDomainSize'
StoreNamedAttribute = "GeometryNodeStoreNamedAttribute"
NamedAttribute = 'GeometryNodeInputNamedAttribute'
@@ -32,7 +29,6 @@ class Nodes:
SampleNearest = "GeometryNodeSampleNearest"
SampleNearestSurface = "GeometryNodeSampleNearestSurface"
-
# Color Menu
ColorRamp = "ShaderNodeValToRGB"
MixRGB = "ShaderNodeMixRGB"
@@ -47,7 +43,7 @@ class Nodes:
CombineColor = 'ShaderNodeCombineColor'
CompCombineColor = 'CompositorNodeCombineColor'
- #bl3.5 additions
+ # bl3.5 additions
SeparateComponents = 'GeometryNodeSeparateComponents'
SetID = 'GeometryNodeSetID'
InterpolateCurves = 'GeometryNodeInterpolateCurves'
@@ -86,6 +82,7 @@ class Nodes:
ReverseCurve = 'GeometryNodeReverseCurve'
SplineLength = 'GeometryNodeSplineLength'
FillCurve = 'GeometryNodeFillCurve'
+ FilletCurve = 'GeometryNodeFilletCurve'
# Curve Primitves
QuadraticBezier = 'GeometryNodeCurveQuadraticBezier'
@@ -130,6 +127,10 @@ class Nodes:
Integer = 'FunctionNodeInputInt'
LightPath = 'ShaderNodeLightPath'
ShortestEdgePath = 'GeometryNodeInputShortestEdgePaths'
+ EdgeNeighbors = 'GeometryNodeInputMeshEdgeNeighbors'
+ ShaderNodeNormalMap = 'ShaderNodeNormalMap'
+ HueSaturationValue = 'ShaderNodeHueSaturation'
+ BlackBody = "ShaderNodeBlackbody"
# Instances
RealizeInstances = "GeometryNodeRealizeInstances"
@@ -158,6 +159,7 @@ class Nodes:
EdgePathToCurve = 'GeometryNodeEdgePathsToCurves'
DeleteGeom = 'GeometryNodeDeleteGeometry'
SplitEdges = 'GeometryNodeSplitEdges'
+ VertexNeighbors = "GeometryNodeInputMeshVertexNeighbors"
# Mesh Primitives
MeshCircle = "GeometryNodeMeshCircle"
@@ -176,7 +178,7 @@ class Nodes:
Composite = "CompositorNodeComposite"
Viewer = "CompositorNodeViewer"
- # Point
+ # Point
DistributePointsOnFaces = "GeometryNodeDistributePointsOnFaces"
PointsToVertices = "GeometryNodePointsToVertices"
PointsToVolume = 'GeometryNodePointsToVolume'
@@ -188,6 +190,7 @@ class Nodes:
VectorCurve = "ShaderNodeVectorCurve"
VectorRotate = "ShaderNodeVectorRotate"
AlignEulerToVector = "FunctionNodeAlignEulerToVector"
+ Displacement = "ShaderNodeDisplacement"
# Volume
VolumeToMesh = 'GeometryNodeVolumeToMesh'
@@ -213,6 +216,10 @@ class Nodes:
ImageTexture = "GeometryNodeImageTexture"
GradientTexture = 'ShaderNodeTexGradient'
ShaderImageTexture = "ShaderNodeTexImage"
+ MagicTexture = "ShaderNodeTexMagic"
+ BrickTexture = 'ShaderNodeTexBrick'
+ CheckerTexture = "ShaderNodeTexChecker"
+ EnvironmentTexture = "ShaderNodeTexEnvironment"
# Shaders
MixShader = "ShaderNodeMixShader"
@@ -229,6 +236,8 @@ class Nodes:
GlassBSDF = "ShaderNodeBsdfGlass"
GlossyBSDF = "ShaderNodeBsdfGlossy"
LayerWeight = "ShaderNodeLayerWeight"
+ UVMap = "ShaderNodeUVMap"
+ Bump = "ShaderNodeBump"
# Layout
Reroute = "NodeReroute"
@@ -248,7 +257,7 @@ class Nodes:
SkyTexture = "ShaderNodeTexSky"
Background = "ShaderNodeBackground"
- #bl3.5 additions
+ # bl3.5 additions
SeparateComponents = 'GeometryNodeSeparateComponents'
SetID = 'GeometryNodeSetID'
InterpolateCurves = 'GeometryNodeInterpolateCurves'
@@ -267,6 +276,7 @@ class Nodes:
OffsetPointinCurve = 'GeometryNodeOffsetPointInCurve'
SplineResolution = 'GeometryNodeInputSplineResolution'
+
'''
Blender doesnt have an automatic way of discovering what properties
exist on a node that might need to be set but are NOT in .inputs. This dict
@@ -310,7 +320,7 @@ class Nodes:
Nodes.RandomValue: ['data_type'],
Nodes.Switch: ['input_type'],
- Nodes.TransferAttribute: ['data_type', 'mapping'],
+ Nodes.TransferAttribute: ['data_type', 'mapping'],
Nodes.SeparateGeometry: ['domain'],
Nodes.MergeByDistance: ['mode'],
@@ -355,33 +365,36 @@ class Nodes:
SINGLETON_NODES = [Nodes.GroupInput, Nodes.GroupOutput, Nodes.MaterialOutput, Nodes.WorldOutput, Nodes.Viewer,
Nodes.Composite, Nodes.RenderLayers, Nodes.LightOutput]
-# Map the type of a socket (ie, .outputs[0].type), to the corresponding value to put into a
-# data_type attr, ie CaptureAttributes data_type. Frustratingly these are not directly related.
+# Map the type of a socket (ie, .outputs[0].type), to the corresponding value to put into a
+# data_type attr, ie CaptureAttributes data_type. Frustratingly these are not directly related.
NODETYPE_TO_DATATYPE = {
'VALUE': 'FLOAT',
'INT': 'INT',
'VECTOR': 'FLOAT_VECTOR',
'FLOAT_COLOR': 'RGBA',
- 'BOOLEAN': 'BOOLEAN'}
+ 'BOOLEAN': 'BOOLEAN'
+}
NODECLASS_TO_DATATYPE = {
'NodeSocketFloat': 'FLOAT',
'NodeSocketInt': 'INT',
'NodeSocketVector': 'FLOAT_VECTOR',
'NodeSocketColor': 'RGBA',
- 'NodeSocketBool': 'BOOLEAN'}
+ 'NodeSocketBool': 'BOOLEAN'
+}
DATATYPE_TO_NODECLASS = {v: k for k, v in NODECLASS_TO_DATATYPE.items()}
NODECLASSES = [k for k in dir(bpy.types) if 'NodeSocket' in k]
PYTYPE_TO_DATATYPE = {
- int: 'INT',
- float: 'FLOAT',
+ int: 'INT',
+ float: 'FLOAT',
np.float32: 'FLOAT',
np.float64: 'FLOAT',
- np.array: 'FLOAT_VECTOR',
+ np.array: 'FLOAT_VECTOR',
bool: 'BOOLEAN'
}
+DATATYPE_TO_PYTYPE = {v: k for k, v in PYTYPE_TO_DATATYPE.items()}
# Each thing containing nodes has a different output node id
OUTPUT_NODE_IDS = {
@@ -408,4 +421,5 @@ class Nodes:
'INT': 'value',
'FLOAT_VECTOR': 'vector',
'FLOAT_COLOR': 'color',
- 'BOOLEAN': 'boolean', }
+ 'BOOLEAN': 'value',
+}
diff --git a/infinigen/core/nodes/node_utils.py b/infinigen/core/nodes/node_utils.py
index 90d511c90..da1e5c35f 100644
--- a/infinigen/core/nodes/node_utils.py
+++ b/infinigen/core/nodes/node_utils.py
@@ -36,13 +36,15 @@ def init_fn(*args, **kwargs):
return registration_fn
-def to_nodegroup(name, singleton, type='GeometryNodeTree'):
+def to_nodegroup(name=None, singleton=False, type='GeometryNodeTree'):
"""Wrapper for initializing and registering new nodegroups."""
- if singleton:
- name += ' (no gc)'
-
def registration_fn(fn):
+ nonlocal name
+ if name is None:
+ name = fn.__name__
+ if singleton:
+ name = name + ' (no gc)'
def init_fn(*args, **kwargs):
if singleton and name in bpy.data.node_groups:
return bpy.data.node_groups[name]
@@ -105,3 +107,17 @@ def resample_node_group(nw: NodeWrangler, scene_seed: int):
if input_socket.name == "Seed":
input_socket.default_value = np.random.randint(1000)
+
+def build_color_ramp(nw, x, positions, colors, mode='HSV'):
+ cr = nw.new_node(Nodes.ColorRamp, input_kwargs={'Fac': x})
+ cr.color_ramp.color_mode = mode
+ elements = cr.color_ramp.elements
+ size = len(positions)
+ assert len(colors) == size
+ if size > 2:
+ for _ in range(size - 2):
+ elements.new(0)
+ for i, (p, c) in enumerate(zip(positions, colors)):
+ elements[i].position = p
+ elements[i].color = c
+ return cr
\ No newline at end of file
diff --git a/infinigen/core/nodes/node_wrangler.py b/infinigen/core/nodes/node_wrangler.py
index 4f0972d76..1ea2d0162 100644
--- a/infinigen/core/nodes/node_wrangler.py
+++ b/infinigen/core/nodes/node_wrangler.py
@@ -1,6 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors:
# - Alexander Raistrick: NodeWrangler class, node linking, expose_input, nodegroup support
# - Zeyu Ma: initial version, fixes, arithmetic utilties
@@ -13,7 +13,7 @@
import warnings
import traceback
import logging
-
+import itertools
from collections.abc import Iterable
import bpy
@@ -22,11 +22,12 @@
from infinigen.core.util.random import random_vector3
from infinigen.core.nodes.node_info import Nodes, NODE_ATTRS_AVAILABLE
from infinigen.core.nodes import node_info
-from infinigen.core.nodes.compatibility import COMPATIBILITY_MAPPINGS
+from .compatibility import COMPATIBILITY_MAPPINGS
logger = logging.getLogger(__name__)
+
class NodeMisuseWarning(UserWarning):
pass
@@ -62,18 +63,13 @@ def warn_with_traceback(message, category, filename, lineno, file=None, line=Non
log.write(warnings.formatwarning(message, category, filename, lineno, line))
-warnings.showwarning = warn_with_traceback
-
-warnings.simplefilter('always', NodeMisuseWarning)
-
-
def isnode(x):
return isinstance(x, (bpy.types.ShaderNode, bpy.types.NodeInternal, bpy.types.GeometryNode))
def infer_output_socket(item):
"""
- Figure out if `item` somehow represents a node with an output we can use.
+ Figure out if `item` somehow represents a node with an output we can use.
If so, return that output socket
"""
@@ -114,14 +110,13 @@ def infer_input_socket(node, input_socket_name):
input_socket = node.inputs[input_socket_name]
if not input_socket.enabled:
- warnings.warn(
+ logger.warning(
f'Attempted to use ({input_socket.name=},{input_socket.type=}) of {node.name=}, but it was '
f'disabled. Either change attrs={{...}}, '
f'change the socket index, or specify the socket by name (assuming two enabled sockets don\'t '
f'share a name).'
f'The input sockets are '
- f'{[(i.name, i.type, ("ENABLED" if i.enabled else "DISABLED")) for i in node.inputs]}.',
- NodeMisuseWarning)
+ f'{[(i.name, i.type, ("ENABLED" if i.enabled else "DISABLED")) for i in node.inputs]}.', )
return input_socket
@@ -156,11 +151,8 @@ def new_value(self, v, label=None):
node.outputs[0].default_value = v
return node
- def new_node(
- self, node_type,
- input_args=None, attrs=None, input_kwargs=None, label=None,
- expose_input=None, compat_mode=True
- ):
+ def new_node(self, node_type, input_args=None, attrs=None, input_kwargs=None, label=None, expose_input=None,
+ compat_mode=True):
if input_args is None:
input_args = []
if input_kwargs is None:
@@ -171,7 +163,7 @@ def new_node(
compat_map = COMPATIBILITY_MAPPINGS.get(node_type)
if compat_mode and compat_map is not None:
- logger.debug(f'Using {compat_map.__name__=} for {node_type=}')
+ # logger.debug(f'Using {compat_map.__name__=} for {node_type=}')
return compat_map(self, node_type, input_args, attrs, input_kwargs)
node = self._make_node(node_type)
@@ -183,7 +175,7 @@ def new_node(
if attrs is not None:
for key, val in attrs.items():
# if key not in NODE_ATTRS_AVAILABLE.get(node.bl_idname, []):
- # warnings.warn(f'Node Wrangler is setting attr {repr(key)} on {node.bl_idname=},
+ # logger.warn(f'Node Wrangler is setting attr {repr(key)} on {node.bl_idname=},
# but it is not in node_info.NODE_ATTRS_AVAILABLE. Please add it so that the transpiler is
# aware')
try:
@@ -202,7 +194,7 @@ def new_node(
else:
input_kwargs["Vector"] = self.new_node(Nodes.InputPosition)
else:
- pass #print(f"{w}, please fix it if you found it causes inconsistency")
+ pass # print(f"{w}, please fix it if you found it causes inconsistency")
input_keyval_list = list(enumerate(input_args)) + list(input_kwargs.items())
for input_socket_name, input_item in input_keyval_list:
@@ -227,7 +219,8 @@ def new_node(
names = [v[1] for v in expose_input]
uniq, counts = np.unique(names, return_counts=True)
if (counts > 1).any():
- raise ValueError(f'expose_input with {names} features duplicate entries. in bl3.5 this is invalid.')
+ raise ValueError(
+ f'expose_input with {names} features duplicate entries. in bl3.5 this is invalid.')
for inp in expose_input:
nodeclass, name, val = inp
self.expose_input(name, val=val, dtype=nodeclass)
@@ -238,7 +231,7 @@ def expose_input(self, name, val=None, attribute=None, dtype=None, use_namednode
'''
Expose an input to the nodegroups interface, making it able to be specified externally
- If this nodegroup is
+ If this nodegroup is
'''
if attribute is not None:
@@ -355,11 +348,10 @@ def _make_node(self, node_type):
f'regular node'
nodegroup_type = {
- 'ShaderNodeTree': 'ShaderNodeGroup',
+ 'ShaderNodeTree': 'ShaderNodeGroup',
'GeometryNodeTree': 'GeometryNodeGroup',
'CompositorNodeTree': 'CompositorNodeGroup'
- }[
- bpy.data.node_groups[node_type].bl_idname]
+ }[bpy.data.node_groups[node_type].bl_idname]
node = self.nodes.new(nodegroup_type)
node.node_tree = bpy.data.node_groups[node_type]
@@ -373,6 +365,27 @@ def get_position_translation_seed(self, i):
self.position_translation_seed[i] = random_vector3()
return self.position_translation_seed[i]
+ def find(self, name):
+ return [n for n in self.nodes if name in type(n).__name__]
+
+ def find_recursive(self, name):
+ return [(self, n) for n in self.find(name)] + sum(
+ (NodeWrangler(n.node_tree).find_recursive(name) for n in self.nodes if n.type == 'GROUP'), [])
+
+ def find_from(self, to_socket):
+ return [l for l in self.links if l.to_socket == to_socket]
+
+ def find_from_recursive(self, name):
+ return [(self, n) for n in self.find(name)] + sum(
+ (NodeWrangler(n.node_tree).find_from_recursive(name) for n in self.nodes if n.type == 'GROUP'), [])
+
+ def find_to(self, from_socket):
+ return [l for l in self.links if l.from_socket == from_socket]
+
+ def find_to_recursive(self, name):
+ return [(self, n) for n in self.find(name)] + sum(
+ (NodeWrangler(n.node_tree).find_to_recursive(name) for n in self.nodes if n.type == 'GROUP'), [])
+
@staticmethod
def is_socket(node):
return isinstance(node, bpy.types.NodeSocket) or isinstance(node, bpy.types.Node)
diff --git a/infinigen/core/nodes/shader_utils.py b/infinigen/core/nodes/shader_utils.py
new file mode 100644
index 000000000..bf4f52f21
--- /dev/null
+++ b/infinigen/core/nodes/shader_utils.py
@@ -0,0 +1,53 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: David Yan
+
+import bpy
+
+def find_displacement_node(mat):
+ links = mat.node_tree.links
+ shader_nodes = mat.node_tree.nodes
+ outputNode = shader_nodes['Material Output']
+ displacement_node = None
+ for link in links:
+ if (link.to_node == outputNode and link.to_socket.name == 'Displacement'):
+ displacement_node = link.from_node
+ break
+ return displacement_node
+
+def convert_shader_displacement(mat : bpy.types.Material):
+
+ mat_copy = mat.copy()
+ mat_copy.name = mat.name + "_copy"
+
+ shader_nodes = mat_copy.node_tree.nodes
+
+ displacement_node = find_displacement_node(mat_copy)
+
+ assert displacement_node is not None
+
+ height = displacement_node.inputs["Height"].default_value
+ mid_level = displacement_node.inputs["Midlevel"].default_value
+ scale = displacement_node.inputs["Scale"].default_value
+
+ new_scale = (height - mid_level) * scale
+ shader_nodes.remove(displacement_node)
+
+ geo_node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
+ group_input = geo_node_group.nodes.new('NodeGroupInput')
+ group_output = geo_node_group.nodes.new('NodeGroupOutput')
+ geo_node_group.outputs.new('NodeSocketGeometry', 'Geometry')
+ geo_node_group.inputs.new('NodeSocketGeometry', 'Geometry')
+ set_pos = geo_node_group.nodes.new('GeometryNodeSetPosition')
+ normal = geo_node_group.nodes.new('GeometryNodeInputNormal')
+ scale = geo_node_group.nodes.new('ShaderNodeVectorMath')
+ scale.operation = 'SCALE'
+ scale.inputs["Scale"].default_value = new_scale
+
+ geo_node_group.links.new(group_input.outputs[0], set_pos.inputs['Geometry'])
+ geo_node_group.links.new(normal.outputs['Normal'], scale.inputs['Vector'])
+ geo_node_group.links.new(scale.outputs['Vector'], set_pos.inputs['Offset'])
+ geo_node_group.links.new(set_pos.outputs['Geometry'], group_output.inputs[0])
+
+ return mat_copy, geo_node_group
diff --git a/infinigen/core/placement/__init__.py b/infinigen/core/placement/__init__.py
index e69de29bb..0b2e9209a 100644
--- a/infinigen/core/placement/__init__.py
+++ b/infinigen/core/placement/__init__.py
@@ -0,0 +1 @@
+from . import camera
\ No newline at end of file
diff --git a/infinigen/core/placement/animation_policy.py b/infinigen/core/placement/animation_policy.py
index 3a6e4e883..7e12cc522 100644
--- a/infinigen/core/placement/animation_policy.py
+++ b/infinigen/core/placement/animation_policy.py
@@ -1,7 +1,9 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Alexander Raistrick
+# Authors:
+# - Alexander Raistrick: Primary author
+# - Zeyu Ma: Animation with path finding
from copy import deepcopy, copy
@@ -10,6 +12,7 @@
import bpy
import mathutils
+from mathutils.bvhtree import BVHTree
import gin
import numpy as np
@@ -17,31 +20,34 @@
from mathutils import Matrix, Vector, Euler
from tqdm import trange, tqdm
+import infinigen.assets.utils.mesh
from infinigen.assets.creatures.util.geometry.curve import Curve
from infinigen.core.util.math import clip_gaussian, lerp
from infinigen.core.util.random import random_general
from infinigen.core.util import blender as butil
+from infinigen.core.placement.path_finding import path_finding
logger = logging.getLogger(__name__)
class PolicyError(ValueError):
pass
-def get_altitude(loc, terrain_bvh, dir=Vector((0.,0.,-1.))):
- *_, straight_down_dist = terrain_bvh.ray_cast(loc, dir)
+def get_altitude(loc, scene_bvh, dir=Vector((0.,0.,-1.))):
+ *_, straight_down_dist = scene_bvh.ray_cast(loc, dir)
return straight_down_dist
@gin.configurable
def walk_same_altitude(
- start_loc,
- sampler,
- bvh,
- filter_func=None,
- fall_ratio=1.5,
- retries=30,
- step_up_height=2,
- ignore_missed_rays=False
+ start_loc,
+ sampler,
+ bvh,
+ filter_func=None,
+ fall_ratio=1.5,
+ retries=30,
+ step_up_height=2,
+ ignore_missed_rays=False,
+ z_move_up=1,
):
'''
@@ -53,7 +59,7 @@ def walk_same_altitude(
pos = start_loc + Vector(sampler())
- pos.z += 1 # move it up a ways, so that it can raycast back down onto something
+ pos.z += z_move_up # move it up a ways, so that it can raycast back down onto something
curr_alt = get_altitude(start_loc, bvh)
new_alt = get_altitude(pos, bvh)
@@ -62,7 +68,7 @@ def walk_same_altitude(
curr_alt = start_loc.z
if new_alt is None:
new_alt = pos.z
-
+
if curr_alt is None or new_alt is None:
if curr_alt is None:
raise PolicyError()
@@ -90,10 +96,10 @@ class AnimPolicyBrownian:
def __init__(self, speed=3, pos_var=15.0):
self.speed = speed
self.pos_var = pos_var
-
+
def __call__(self, obj, frame_curr, bvh, retry_pct):
-
+
speed = random_general(self.speed)
sampler = lambda: N(0, [self.pos_var, self.pos_var, 0.5])
pos = walk_same_altitude(obj.location, sampler, bvh)
@@ -102,7 +108,7 @@ def __call__(self, obj, frame_curr, bvh, retry_pct):
rot = np.array(obj.rotation_euler) + np.deg2rad(N(0, [5, 0, 5], 3))
return Vector(pos), Vector(rot), time, "BEZIER"
-
+
@gin.configurable
class AnimPolicyPan:
@@ -112,7 +118,7 @@ def __init__(self, speed=3, dist=("uniform", 5, 20), rot_var=[10, 0, 20]):
self.rot_var = rot_var
def __call__(self, obj, frame_curr, bvh, retry_pct):
-
+
speed = random_general(self.speed)
def sampler():
theta = U(0, 2*np.pi)
@@ -125,15 +131,15 @@ def sampler():
rot = np.array(obj.rotation_euler) + np.deg2rad(N(0, self.rot_var, 3))
return Vector(pos), Vector(rot), time, "LINEAR"
-
+
@gin.configurable
class AnimPolicyRandomForwardWalk:
def __init__(
- self,
+ self,
forward_vec,
- speed=2,
- yaw_dist=("uniform", -30, 30),
+ speed=2,
+ yaw_dist=("uniform", -30, 30),
altitude_var=0,
step_range=(1, 10),
rot_vars=[5, 0, 5],
@@ -163,7 +169,7 @@ def sampler():
rot = np.array(obj.rotation_euler) + np.deg2rad(N(0, self.rot_vars, 3))
return Vector(pos), Vector(rot), time, 'BEZIER'
-
+
@gin.configurable
class AnimPolicyRandomWalkLookaround:
@@ -174,9 +180,10 @@ def __init__(
yaw_sampler=('uniform',-20, 20),
step_range=('clip_gaussian', 3, 5, 0.5, 10),
rot_vars=(5, 0, 5),
- motion_dir_zoff=('clip_gaussian', 0, 90, 0, 180)
+ motion_dir_zoff=('clip_gaussian', 0, 90, 0, 180),
+ force_single_keyframe=False
):
-
+
self.speed = random_general(speed)
self.step_speed_mult = step_speed_mult
@@ -187,11 +194,13 @@ def __init__(
self.motion_dir_euler = None
self.motion_dir_zoff = motion_dir_zoff
+ self.force_single_keyframe = force_single_keyframe
+
def __call__(self, obj, frame_curr, bvh, retry_pct):
if self.motion_dir_euler is None:
self.motion_dir_euler = copy(obj.rotation_euler)
-
+
self.motion_dir_euler[2] += np.deg2rad(random_general(self.motion_dir_zoff))
orig_motion_dir_euler = copy(self.motion_dir_euler)
@@ -209,21 +218,24 @@ def sampler():
time = np.linalg.norm(pos - obj.location) / step_speed
rot = np.array(obj.rotation_euler) + np.deg2rad(N(0, self.rot_vars, 3))
+ if self.force_single_keyframe:
+ time = bpy.context.scene.frame_end - frame_curr
+
return Vector(pos), Vector(rot), time, 'BEZIER'
-
+
@gin.configurable
class AnimPolicyFollowObject:
def __init__(
- self, target_obj, pois, bvh,
- zrot_vel_var=20,
- follow_zrot=0,
+ self, target_obj, pois, bvh,
+ zrot_vel_var=20,
+ follow_zrot=0,
follow_rad_mult=('uniform', 1, 6),
alt_mult=('uniform', 0.25, 1)
):
self.pois = pois
- self.target_obj = target_obj
+ self.target_obj = target_obj
self.follow_zrot = follow_zrot
self.zrot_vel = np.deg2rad(N(0, zrot_vel_var))
self.rad_vel = N(0, 0.03)
@@ -259,7 +271,7 @@ def reset(self):
if alt is None:
logger.warning(f'In AnimPolicyFollowObject.reset(), got {alt=}')
if alt is not None and alt < 2:
- self.target_obj.location *= self.target_obj.location.z / 2
+ self.target_obj.location *= self.target_obj.location.z / 2
for c in self.target_obj.constraints:
self.target_obj.constraints.remove(c)
@@ -276,7 +288,7 @@ def __call__(self, obj, frame_curr, bvh, retry_pct):
frame_next = min(t for t in ts if t > frame_curr)
except (ValueError, AttributeError): # no next frame, or no animation_data.action
frame_next = frame_curr + bpy.context.scene.render.fps
-
+
time = (frame_next - frame_curr) / bpy.context.scene.render.fps
bpy.context.scene.frame_set(frame_curr)
@@ -298,13 +310,13 @@ def __call__(self, obj, frame_curr, bvh, retry_pct):
return Vector(pos), None, time, 'BEZIER'
def validate_keyframe_range(
- obj,
- start_frame, end_frame,
+ obj,
+ start_frame, end_frame,
bvhtree, validate_pose_func=None,
stride=5, # runs faster but imperfect precision
check_straight_line=True # rules out proposals faster, but has imperfect precision
):
-
+
last_pos = deepcopy(obj.location)
def freespace_ray_check(a, b):
@@ -324,27 +336,29 @@ def freespace_ray_check(a, b):
logger.debug(f'{frame_idx=} freespace_ray_check failed')
return False
- if validate_pose_func is not None and not validate_pose_func(obj):
+ if validate_pose_func is not None and not validate_pose_func(obj):
# technically we should validate against all cameras, but this would be expensive
logger.debug(f'{frame_idx} validate_pose_func failed')
- return False
+ return False
last_pos = deepcopy(obj.location)
return True
def try_animate_trajectory(
- obj, bvh, policy_func,
+ obj: bpy.types.Object,
+ bvh: BVHTree,
+ policy_func,
keyframe, duration_frames,
validate_pose_func=None,
max_step_tries=50,
verbose=True,
):
-
+
frame_curr = bpy.context.scene.frame_start
pbar = tqdm(total=duration_frames) if verbose else None
while frame_curr < bpy.context.scene.frame_start + duration_frames:
-
+
orig_loc = copy(obj.location)
orig_rot = copy(obj.rotation_euler)
for retry in range(max_step_tries):
@@ -352,19 +366,19 @@ def try_animate_trajectory(
bpy.context.view_layer.update()
try:
loc, rot, duration, interp = policy_func(
- obj,
- frame_curr=frame_curr,
- retry_pct=retry/max_step_tries,
+ obj,
+ frame_curr=frame_curr,
+ retry_pct=retry/max_step_tries,
bvh=bvh
)
except PolicyError as e:
logger.debug(f'PolicyError on {retry=} {e=}')
continue
-
+
step_frames = int(duration * bpy.context.scene.render.fps) + 1
step_end_frame = frame_curr + step_frames
- keyframe(loc, rot, step_end_frame, interp='BEZIER')
+ keyframe(obj, loc, rot, step_end_frame, interp='BEZIER')
if not validate_keyframe_range(obj, frame_curr, step_end_frame, bvh, validate_pose_func):
logger.debug(f'validate_keyframe_range failed on moving {obj.location} to {loc}')
@@ -374,7 +388,7 @@ def try_animate_trajectory(
continue
obj.keyframe_delete(data_path=fc.data_path, frame=step_end_frame)
continue
-
+
if verbose:
pbar.update(min(step_frames, duration_frames - frame_curr)) # dont overshoot the pbar, it makes the formatting not nice
@@ -388,44 +402,150 @@ def try_animate_trajectory(
return True
+
+@gin.configurable
+def try_animate_with_pathfinding(
+ obj, bvh, policy_func,
+ keyframe, duration_frames,
+ validate_pose_func,
+ max_step_tries,
+ verbose,
+ bounding_box,
+ turning_limit_degree=10,
+):
+
+ frame_curr = bpy.context.scene.frame_start
+ pbar = tqdm(total=duration_frames) if verbose else None
+ while frame_curr < bpy.context.scene.frame_start + duration_frames:
+
+ orig_loc = copy(obj.location)
+ orig_rot = copy(obj.rotation_euler)
+ for retry in range(max_step_tries):
+ obj.location, obj.rotation_euler = orig_loc, orig_rot
+ bpy.context.view_layer.update()
+ try:
+ loc, rot, duration, interp = policy_func(
+ obj,
+ frame_curr=frame_curr,
+ retry_pct=retry/max_step_tries,
+ bvh=bvh
+ )
+ except PolicyError as e:
+ logger.debug(f'PolicyError on {retry=} {e=}')
+ continue
+
+ bpy.context.scene.frame_set(frame_curr)
+ last_pose = (deepcopy(obj.location), deepcopy(obj.rotation_euler))
+ keyframe(obj, loc, rot, frame_curr+1, interp='BEZIER')
+ bpy.context.scene.frame_set(frame_curr+1)
+
+ valid_target = True
+ if validate_pose_func is not None and not validate_pose_func(obj):
+ valid_target = False
+
+ # if valid_target:
+ # bpy.ops.wm.save_mainfile(filepath="/u/zeyum/p/debug.blend")
+ # assert(0)
+
+ for fc in obj.animation_data.action.fcurves:
+ if fc.data_path == "":
+ continue
+ obj.keyframe_delete(data_path=fc.data_path, frame=frame_curr+1)
+
+ if not valid_target:
+ logger.debug(f'validate_pose_func at target pose failed, aborting path finding')
+ continue
+
+
+
+ bounded = True
+ current_pose = (loc, rot)
+ for i in range(3):
+ if current_pose[0][i] < bounding_box[0][i] or current_pose[0][i] >= bounding_box[1][i]:
+ bounded = False
+ if not bounded:
+ logger.debug(f'target pose out of bound, aborting path finding')
+ continue
+
+ poses = path_finding(bvh, bounding_box, last_pose, current_pose)
+
+ if poses is None:
+ logger.debug(f'path not found, aborting')
+ continue
+
+ base_length = (last_pose[0] - current_pose[0]).length
+ scaling = poses[-1][0] / base_length
+
+ step_frames = int(duration * bpy.context.scene.render.fps * scaling) + 1
+ step_end_frame = frame_curr + step_frames
+
+ turning_too_fast = False
+ for i in range(len(poses) - 1):
+ rotation_euler0 = poses[i][2]
+ rotation_euler1 = poses[i+1][2]
+ if abs(rotation_euler0.z - rotation_euler1.z) > turning_limit_degree / 180 * np.pi * step_frames * (poses[i+1][0] - poses[i][0]) / poses[-1][0]:
+ turning_too_fast = True
+ break
+ if turning_too_fast:
+ logger.debug(f'path turns too fast, aborting')
+ continue
+
+ for l, location, rotation_euler in poses:
+ t = l / poses[-1][0]
+ keyframe(obj, location, rotation_euler, round(frame_curr + (step_end_frame - frame_curr) * t), interp='LINEAR')
+
+ if verbose:
+ pbar.update(min(step_frames, duration_frames - frame_curr)) # dont overshoot the pbar, it makes the formatting not nice
+
+ break # we found a good pose
+
+ else: # for-else block triggers when for loop terminates w/o a break statement
+ return False
+
+ frame_curr = step_end_frame
+ bpy.context.scene.frame_current = frame_curr
+
+ return True
+
+def keyframe(obj, loc, rot, t, interp='BEZIER'):
+
+ if obj.animation_data is not None and obj.animation_data.action is not None:
+ for fc in obj.animation_data.action.fcurves:
+ for kp in fc.keyframe_points:
+ if kp.co > t:
+ raise ValueError(f'Unexpected out-of-order keyframing {kp.co=}, {t=}')
+
+ if loc is not None:
+ obj.location = loc
+ obj.keyframe_insert(data_path="location", frame=t),
+
+ if rot is not None:
+ obj.rotation_euler = rot
+ obj.keyframe_insert(data_path="rotation_euler", frame=t)
+
+ for fc in obj.animation_data.action.fcurves:
+ for k in fc.keyframe_points:
+ if k.co[0] == t:
+ k.interpolation = interp
+
@gin.configurable
def animate_trajectory(
obj, bvh, policy_func,
validate_pose_func=None,
max_step_tries=25,
max_full_retries=10,
- default_interpolation='BEZIER',
retry_rotation=False,
verbose=True,
fatal=False,
reverse_time=False,
+ bounding_box=None,
+ path_finding_enabled=False,
):
duration_frames = (bpy.context.scene.frame_end - bpy.context.scene.frame_start)
duration_sec = duration_frames / bpy.context.scene.render.fps
if duration_sec < 1e-3:
return
- def keyframe(loc, rot, t, interp=default_interpolation):
-
- if obj.animation_data is not None and obj.animation_data.action is not None:
- for fc in obj.animation_data.action.fcurves:
- for kp in fc.keyframe_points:
- if kp.co > t:
- raise ValueError(f'Unexpected out-of-order keyframing {kp.co=}, {t=}')
-
- if loc is not None:
- obj.location = loc
- obj.keyframe_insert(data_path="location", frame=t),
-
- if rot is not None:
- obj.rotation_euler = rot
- obj.keyframe_insert(data_path="rotation_euler", frame=t)
-
- for fc in obj.animation_data.action.fcurves:
- for k in fc.keyframe_points:
- if k.co[0] == t:
- k.interpolation = interp
-
obj_orig_loc = copy(obj.location)
obj_orig_rot = copy(obj.rotation_euler)
@@ -438,10 +558,13 @@ def keyframe(loc, rot, t, interp=default_interpolation):
obj.rotation_euler.z = U(0, 2 * np.pi)
if hasattr(policy_func, 'reset'):
- policy_func.reset()
+ infinigen.assets.utils.mesh.reset_preset()
- keyframe(obj.location, obj.rotation_euler, 0, interp='LINEAR')
- if try_animate_trajectory(obj, bvh, policy_func, keyframe, duration_frames, validate_pose_func, max_step_tries, verbose):
+ keyframe(obj, obj.location, obj.rotation_euler, 0, interp='LINEAR')
+ try_animate_trajectory_func = try_animate_trajectory if not path_finding_enabled else try_animate_with_pathfinding
+ args = [obj, bvh, policy_func, keyframe, duration_frames, validate_pose_func, max_step_tries, verbose]
+ if path_finding_enabled: args.append(bounding_box)
+ if try_animate_trajectory_func(*args):
if reverse_time:
kf_locs = []
kf_rots = []
@@ -460,7 +583,7 @@ def keyframe(loc, rot, t, interp=default_interpolation):
))
obj.animation_data_clear()
for i, t in enumerate(kf_ts):
- keyframe(kf_locs[i], kf_rots[i], bpy.context.scene.frame_end + bpy.context.scene.frame_start - t, interp='LINEAR')
+ keyframe(obj, kf_locs[i], kf_rots[i], bpy.context.scene.frame_end + bpy.context.scene.frame_start - t, interp='LINEAR')
# bpy.context.scene.frame_set(bpy.context.scene.frame_end)
# obj.keyframe_insert(data_path="location", frame=bpy.context.scene.frame_end)
# obj.keyframe_insert(data_path="rotation_euler", frame=bpy.context.scene.frame_end)
@@ -474,9 +597,10 @@ def keyframe(loc, rot, t, interp=default_interpolation):
else:
logger.warning(err)
return
-
+
+
def policy_create_bezier_path(start_pose_obj, bvh, policy_func, to_mesh=False, eval_offset=(0,0,0), **kwargs):
-
+
eval_offset = Vector(eval_offset)
# animate a dummy using the policy
diff --git a/infinigen/core/placement/camera.py b/infinigen/core/placement/camera.py
index aa5127e91..ea92abba7 100644
--- a/infinigen/core/placement/camera.py
+++ b/infinigen/core/placement/camera.py
@@ -3,18 +3,20 @@
# Authors:
# - Zeyu Ma, Lahav Lipson: Stationary camera selection
-# - Alex Raistrick: Refactor into proposal/validate, camera animation
+# - Alexander Raistrick: Refactor into proposal/validate, camera animation
# - Lingjie Mei: get_camera_trajectory
from random import sample
import sys
import warnings
+import typing
from copy import deepcopy, copy
from functools import partial
from itertools import chain
import logging
from pathlib import Path
+from dataclasses import dataclass
from numpy.random import uniform as U
@@ -36,10 +38,13 @@
from . import animation_policy
from infinigen.core.util import blender as butil
+from infinigen.core.util.blender import SelectObjects, delete
from infinigen.core.util.logging import Timer
from infinigen.core.util.math import clip_gaussian, lerp
from infinigen.core.util import camera
from infinigen.core.util.random import random_general
+from infinigen.core.tagging import tag_system
+from infinigen.core.util.organization import SelectionCriterions
from infinigen.tools.suffixes import get_suffix
@@ -201,7 +206,7 @@ def set_camera(
if focus_dist is not None:
camera.data.dof.keyframe_insert(data_path="focus_distance", frame=frame)
-def terrain_camera_query(cam, terrain_bvh, terrain_tags_queries, vertexwise_min_dist, min_dist=0):
+def terrain_camera_query(cam, scene_bvh, terrain_tags_queries, vertexwise_min_dist, min_dist=0):
dists = []
sensor_coords, pix_it = get_sensor_coords(cam, sparse=True)
@@ -209,7 +214,7 @@ def terrain_camera_query(cam, terrain_bvh, terrain_tags_queries, vertexwise_min_
for x,y in pix_it:
direction = (sensor_coords[y,x] - cam.matrix_world.translation).normalized()
- _, _, index, dist = terrain_bvh.ray_cast(cam.matrix_world.translation, direction)
+ _, _, index, dist = scene_bvh.ray_cast(cam.matrix_world.translation, direction)
if dist is None:
continue
dists.append(dist)
@@ -217,6 +222,7 @@ def terrain_camera_query(cam, terrain_bvh, terrain_tags_queries, vertexwise_min_
dist < min_dist or
(vertexwise_min_dist is not None and dist < vertexwise_min_dist[index])
):
+ logger.debug(f'Found {dist=} < {min_dist=}')
dists = None # means dist < min
break
for q in terrain_tags_queries:
@@ -226,59 +232,56 @@ def terrain_camera_query(cam, terrain_bvh, terrain_tags_queries, vertexwise_min_
return dists, terrain_tags_queries_counts, n_pix
+@dataclass
+class CameraProposal:
+ loc: np.array
+ rot: np.array
+ focal_length: float
+
+ def apply(self, cam):
+ cam.location = self.loc
+ cam.rotation_euler = self.rot
+ cam.data.lens = self.focal_length
+
@gin.configurable
def camera_pose_proposal(
- terrain_bvh,
- terrain_bbox,
- altitude=2,
+ scene_bvh,
+ location_sample: typing.Callable | tuple,
+ altitude=('uniform', 1.5, 2.5),
roll=0,
yaw=('uniform', -180, 180),
pitch=90,
- headspace_retries=30,
+ focal_length=50,
override_loc=None,
- override_rot=None
):
+
+ if isinstance(location_sample, tuple):
+ location_sample = Vector(location_sample)
+ location_sample = lambda: location_sample
- if override_loc is None:
- loc = np.random.uniform(*terrain_bbox)
-
- alt = animation_policy.get_altitude(loc, terrain_bvh)
- if alt is None:
- return None
-
- headspace = animation_policy.get_altitude(loc, terrain_bvh, dir=Vector((0, 0, 1)))
- for headspace_retry in range(headspace_retries):
- desired_alt = random_general(altitude)
- if desired_alt is None:
- zoff = 0
- break
- zoff = desired_alt - alt
- if headspace is None:
- break
- if desired_alt < headspace:
- break
- logger.debug(f'camera_pose_proposal failed {headspace_retry=} due to {headspace=} {desired_alt=} {alt=}')
- else: # for-else triggers if no break, IE no acceptable voffset was found
- logger.warning(f'camera_pose_proposal found no zoff for {loc=} after {headspace_retries=}')
- return None
-
- loc[2] = loc[2] + zoff
- if loc[2] > terrain_bbox[1][2] or loc[2] < terrain_bbox[0][2]:
- return None
- else:
+ if override_loc is not None:
loc = Vector(random_general(override_loc))
- if override_rot:
- rot = np.deg2rad(override_rot)
+ elif altitude is None:
+ loc = location_sample()
else:
- rot = np.deg2rad([random_general(pitch), random_general(roll), random_general(yaw)])
+ loc = location_sample()
+ curr_alt = animation_policy.get_altitude(loc, scene_bvh)
+ if curr_alt is None:
+ logger.debug(f'camera_pose_proposal got {curr_alt=} for {loc=}')
+ butil.spawn_empty(f'fail')
+ return None
+ desired_alt = random_general(altitude)
+ loc[2] = loc[2] + desired_alt - curr_alt
- return loc, rot
+ rot = np.deg2rad([random_general(pitch), random_general(roll), random_general(yaw)])
+ focal_length = random_general(focal_length)
+ return CameraProposal(loc, rot, focal_length)
@gin.configurable
def keep_cam_pose_proposal(
cam,
terrain,
- terrain_bvh,
+ scene_bvh,
placeholders_kd,
camera_selection_answers,
vertexwise_min_dist,
@@ -305,7 +308,7 @@ def keep_cam_pose_proposal(
return None
dists, camera_selection_answers_counts, n_pix = terrain_camera_query(
- cam, terrain_bvh, camera_selection_answers, vertexwise_min_dist, min_dist=min_terrain_distance)
+ cam, scene_bvh, camera_selection_answers, vertexwise_min_dist, min_dist=min_terrain_distance)
if dists is None:
logger.debug('keep_cam_pose_proposal rejects terrain dists')
@@ -313,29 +316,29 @@ def keep_cam_pose_proposal(
coverage = len(dists)/n_pix
if coverage < terrain_coverage_range[0] or coverage > terrain_coverage_range[1]:
+ logger.debug(f'keep_cam_pose_proposal rejects {coverage=} for {terrain_coverage_range=}')
return None
-
- if terrain is None:
- return 0
-
- if terrain_sdf <= 0:
+
+ if terrain is not None and terrain_sdf <= 0:
logger.debug(f'keep_cam_pose_proposal rejects {terrain_sdf=}')
return None
if rparams := camera_selection_ratio:
for q in rparams:
- if type(q) is tuple and q[0] == "closeup":
+ if type(q) is tuple and q[0] == SelectionCriterions.CloseUp:
closeup = len([d for d in dists if d < q[1]])/n_pix
if closeup < rparams[q][0] or closeup > rparams[q][1]:
+ logger.debug(f'keep_cam_pose_proposal rejects {closeup=} for {q=}')
return None
else:
minv, maxv = rparams[q][0], rparams[q][1]
if q in camera_selection_answers_counts:
ratio = camera_selection_answers_counts[q] / n_pix
if ratio < minv or ratio > maxv:
+ logger.debug(f'keep_cam_pose_proposal rejects {ratio=} for {q=}')
return None
- return np.std(dists)
+ return np.std(dists) + 1.5 * np.min(dists)
@gin.configurable
class AnimPolicyGoToProposals:
@@ -354,93 +357,152 @@ def __call__(self, camera_rig, frame_curr, retry_pct, bvh):
res = camera_pose_proposal(bvh, bbox)
if res is None:
continue
- pos, rot = res
- pos = np.array(pos)
- if np.linalg.norm(pos - np.array(camera_rig.location)) < self.min_dist:
+ dist = np.linalg.norm(np.array(res.loc) - np.array(camera_rig.location))
+ if dist < self.min_dist:
continue
break
else:
raise animation_policy.PolicyError(f'{__name__} found no keyframe after {self.retries=}')
- time = np.linalg.norm(pos - camera_rig.location) / random_general(self.speed)
- return Vector(pos), Vector(rot), time, 'BEZIER'
+ time = dist / random_general(self.speed)
+ return Vector(res.loc), Vector(res.rot), time, 'BEZIER'
@gin.configurable
def compute_base_views(
cam, n_views,
terrain,
- terrain_bvh,
- terrain_bbox,
+ scene_bvh,
+ location_sample: typing.Callable,
placeholders_kd=None,
camera_selection_answers={},
vertexwise_min_dist=None,
camera_selection_ratio=None,
min_candidates_ratio=20,
- max_tries=10000,
+ max_tries=30000,
+ visualize=False
):
potential_views = []
n_min_candidates = int(min_candidates_ratio * n_views)
with tqdm(total=n_min_candidates, desc='Searching for camera viewpoints') as pbar:
for it in range(1, max_tries):
- props = camera_pose_proposal(terrain_bvh=terrain_bvh, terrain_bbox=terrain_bbox)
- if props is None: continue
- loc, rot = props
+ props = camera_pose_proposal(
+ scene_bvh=scene_bvh,
+ location_sample=location_sample
+ )
+
+ if props is None:
+ logger.debug(f'{camera_pose_proposal.__name__} returned {props=} for {it=}')
+ continue
+
+ props.apply(cam)
- cam.location = loc
- cam.rotation_euler = rot
criterion = keep_cam_pose_proposal(
- cam, terrain, terrain_bvh, placeholders_kd,
+ cam, terrain, scene_bvh, placeholders_kd,
camera_selection_answers=camera_selection_answers,
vertexwise_min_dist=vertexwise_min_dist,
camera_selection_ratio=camera_selection_ratio,
)
+
+ if visualize:
+ criterion_str = f'{criterion:.2f}' if criterion is not None else 'None'
+ marker = butil.spawn_empty(f'attempt_{it}_{criterion_str}')
+ marker.location = cam.location
+ marker.rotation_euler = cam.rotation_euler
+
if criterion is None:
+ logger.debug(f'{it=} {criterion=}')
continue
# Compute focus distance
destination = cam.matrix_world @ Vector((0.,0.,-1.))
forward_dir = (destination - cam.location).normalized()
- *_, straight_ahead_dist = terrain_bvh.ray_cast(cam.location, forward_dir)
+ *_, straight_ahead_dist = scene_bvh.ray_cast(cam.location, forward_dir)
- potential_views.append((criterion, deepcopy(cam.location), deepcopy(cam.rotation_euler), straight_ahead_dist))
+ potential_views.append((criterion, deepcopy(props), straight_ahead_dist))
pbar.update(1)
if len(potential_views) >= n_min_candidates:
break
if len(potential_views) < n_views:
+ if visualize:
+ butil.save_blend('compute_base_views-failed.blend')
raise ValueError(f'Could not find {n_views} camera views')
- return sorted(potential_views, reverse=True)[:n_views]
-
-@gin.configurable
-def camera_selection_keep_in_animation(**kwargs):
- return kwargs
-
-@gin.configurable
-def camera_selection_tags_ratio(**kwargs):
- keep_in_animation = camera_selection_keep_in_animation()
- d = {}
- for k in kwargs:
- d[k] = (*kwargs[k], k in keep_in_animation and keep_in_animation[k])
- return d
-
-@gin.configurable
-def camera_selection_ranges_ratio(**kwargs):
- keep_in_animation = camera_selection_keep_in_animation()
- d = {}
- for k in kwargs:
- d[kwargs[k][:-2]] = (kwargs[k][-2], kwargs[k][-1], k in keep_in_animation and keep_in_animation[k])
- return d
+ views = sorted(potential_views, reverse=True)
+
+ return views[:n_views]
+
+
+def build_bvh_and_attrs(objs, tags_queries):
+ dup_objs = []
+ for obj in objs:
+ with SelectObjects(obj):
+ bpy.ops.object.duplicate(linked=0,mode='TRANSLATION')
+ dup_objs.append(bpy.context.view_layer.objects.active)
+ for obj in dup_objs:
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_all(action='SELECT')
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ with SelectObjects(dup_objs[0]):
+ for obj in dup_objs[1:]:
+ obj.select_set(True)
+ bpy.ops.object.join()
+ obj = bpy.context.view_layer.objects.active
+ bvh = BVHTree.FromObject(obj, bpy.context.evaluated_depsgraph_get())
+ from infinigen.terrain.utils import Mesh
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
+ mesh = Mesh(obj=obj)
+ delete(obj)
+
+ camera_selection_answers = {}
+ for q0 in tags_queries:
+ if type(q0) is not tuple:
+ q = (q0,)
+ else:
+ q = q0
+ if q[0] in [SelectionCriterions.CloseUp]: continue
+ if q[0] == SelectionCriterions.Altitude:
+ min_altitude, max_altitude = q[1:3]
+ altitude = mesh.vertices[:, 2]
+ camera_selection_answers[q0] = mesh.facewise_mean((altitude > min_altitude) & (altitude < max_altitude))
+ else:
+ camera_selection_answers[q0] = np.zeros(len(mesh.faces), dtype=bool)
+ for key in tag_system.tag_dict:
+ if set(q).issubset(set(key.split('.'))):
+ camera_selection_answers[q0] |= (mesh.face_attributes["MaskTag"] == tag_system.tag_dict[key]).reshape(-1)
+ return bvh, camera_selection_answers
def camera_selection_preprocessing(
terrain,
- terrain_mesh,
+ scene_objs,
+ tags_ratio: dict = None,
+ ranges_ratio: dict = None,
+ anim_criterion_keys: dict = None,
):
- camera_selection_ratio = camera_selection_tags_ratio()
- camera_selection_ratio.update(camera_selection_ranges_ratio())
+
+ if tags_ratio is None:
+ tags_ratio = {}
+ if ranges_ratio is None:
+ ranges_ratio = {}
+ if anim_criterion_keys is None:
+ anim_criterion_keys = {}
+
+ # preprocessing code adapted from mazeyu's original gin-oriented solution
+ tags_ratio = {
+ k: (*v, anim_criterion_keys.get(k, False))
+ for k, v in tags_ratio.items()
+ }
+ ranges_ratio = {
+ v[:-2]: (v[-2], v[-1], anim_criterion_keys.get(k, False))
+ for k, v in ranges_ratio.items()
+ }
+
+ all_selection_ratios = {**tags_ratio, **ranges_ratio}
+
with Timer('Building placeholders KDTree'):
placeholders = list(chain.from_iterable(
@@ -451,46 +513,81 @@ def camera_selection_preprocessing(
placeholders_kd = butil.joined_kd(placeholders, include_origins=True)
if terrain is None:
- bvh = BVHTree.FromObject(terrain_mesh, bpy.context.evaluated_depsgraph_get())
- return dict(
- terrain=None,
- terrain_bvh=bvh,
- placeholders_kd=placeholders_kd
- )
-
- with Timer(f'Building terrain BVHTree'):
- terrain_bvh, camera_selection_answers, vertexwise_min_dist = terrain.build_terrain_bvh_and_attrs(camera_selection_ratio.keys())
+ scene_bvh, camera_selection_answers = build_bvh_and_attrs(scene_objs, all_selection_ratios.keys())
+ vertexwise_min_dist = None
+ else:
+ scene_bvh, camera_selection_answers, vertexwise_min_dist = terrain.build_terrain_bvh_and_attrs(all_selection_ratios.keys())
return dict(
terrain=terrain,
- terrain_bvh=terrain_bvh,
+ scene_bvh=scene_bvh,
+ camera_selection_ratio=all_selection_ratios,
camera_selection_answers=camera_selection_answers,
vertexwise_min_dist=vertexwise_min_dist,
placeholders_kd=placeholders_kd,
- camera_selection_ratio=camera_selection_ratio,
)
+@node_utils.to_nodegroup('geo_distrib', singleton=True, type='GeometryNodeTree')
+def geo_distrib_random_points(nw: NodeWrangler):
+ input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketGeometry', 'Geometry', None)])
+ distribute = nw.new_node(Nodes.DistributePointsOnFaces, input_kwargs={
+ 'Mesh': input.outputs['Geometry'],
+ 'Density': 500
+ })
+ verts = nw.new_node(Nodes.PointsToVertices, [distribute])
+ output = nw.new_node(Nodes.GroupOutput, input_kwargs={"Geometry": verts})
+
+def sample_random_locs(surface: bpy.types.Object, eps=0.01):
+ # HACK implementation - uses blender geonodes' uniform surface sample, im fairly sure theres a numpy impl somewhere in the repo
+ surface = butil.copy(surface)
+ butil.apply_transform(surface, loc=True, rot=True, scale=True)
+ butil.modify_mesh(
+ surface,
+ "NODES",
+ node_group=geo_distrib_random_points(),
+ apply=True
+ )
+ locs = np.array([v.co for v in surface.data.vertices])
+ locs[:, -1] += eps
+ butil.delete(surface)
+ return locs
+
@gin.configurable
def configure_cameras(
cam_rigs,
- bounding_box,
- scene_preprocessed,
+ scene_preprocessed: dict,
+ init_bounding_box: tuple[np.array, np.array] = None,
+ init_surfaces: list[bpy.types.Object] = None,
):
bpy.context.view_layer.update()
dummy_camera = spawn_camera()
+ if init_bounding_box is not None:
+ location_sample = lambda: np.random.uniform(*init_bounding_box)
+ elif init_surfaces is not None:
+ random_locs = sample_random_locs(init_surfaces)
+ def location_sample():
+ loc = Vector(random_locs[np.random.randint(len(random_locs)), :])
+ loc.z += 1e-3
+ return loc
+ else:
+ raise ValueError('Either init_bounding_box or init_surfaces must be provided')
+
base_views = compute_base_views(
dummy_camera,
n_views=len(cam_rigs),
- terrain_bbox=bounding_box,
+ location_sample=location_sample,
**scene_preprocessed
)
for view, cam_rig in zip(base_views, cam_rigs):
- score, loc, rot, focus_dist = view
- cam_rig.location = loc
- cam_rig.rotation_euler = rot
+ score, props, focus_dist = view
+ cam_rig.location = props.loc
+ cam_rig.rotation_euler = props.rot
+
+ for cam in cam_rig.children:
+ cam.data.lens = props.focal_length
if focus_dist is not None:
for cam in cam_rig.children:
@@ -501,7 +598,8 @@ def configure_cameras(
@gin.configurable
def animate_cameras(
- cam_rigs,
+ cam_rigs,
+ bounding_box,
scene_preprocessed,
pois=None,
follow_poi_chance=0.0,
@@ -517,7 +615,7 @@ def animate_cameras(
anim_valid_pose_func = partial(
keep_cam_pose_proposal,
placeholders_kd=scene_preprocessed['placeholders_kd'],
- terrain_bvh=scene_preprocessed['terrain_bvh'],
+ scene_bvh=scene_preprocessed['scene_bvh'],
terrain=scene_preprocessed['terrain'],
vertexwise_min_dist=scene_preprocessed['vertexwise_min_dist'],
camera_selection_answers=animation_answers,
@@ -526,12 +624,12 @@ def animate_cameras(
for cam_rig in cam_rigs:
- if policy_registry is None:
+ if policy_registry is None:
if U() < follow_poi_chance and pois is not None and len(pois):
policy = animation_policy.AnimPolicyFollowObject(
- target_obj=cam_rig,
- pois=pois,
- bvh=scene_preprocessed['terrain_bvh']
+ target_obj=cam_rig,
+ pois=pois,
+ bvh=scene_preprocessed['scene_bvh']
)
else:
policy = animation_policy.AnimPolicyRandomWalkLookaround()
@@ -541,12 +639,13 @@ def animate_cameras(
logger.info(f'Animating {cam_rig=} using {policy=}')
animation_policy.animate_trajectory(
- cam_rig,
- scene_preprocessed['terrain_bvh'],
+ cam_rig,
+ scene_preprocessed['scene_bvh'],
policy_func=policy,
validate_pose_func=anim_valid_pose_func,
verbose=True,
- fatal=True
+ fatal=True,
+ bounding_box=bounding_box,
)
@gin.configurable
@@ -606,3 +705,4 @@ def save_camera_parameters(camera_ids, output_folder, frame, use_dof=False):
color_depth = colorize_depth(depth_output)
imageio.imwrite(f"color_depth.png", color_depth)
+
diff --git a/infinigen/core/placement/density.py b/infinigen/core/placement/density.py
index 19d97616d..fb830526a 100644
--- a/infinigen/core/placement/density.py
+++ b/infinigen/core/placement/density.py
@@ -1,7 +1,9 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Alexander Raistrick
+# Authors:
+# - Alexander Raistrick: The majority part
+# - Zeyu Ma: Selection based on tag
import pdb
@@ -23,6 +25,30 @@ def set_tag_dict(tag_dict_):
global tag_dict
tag_dict = tag_dict_
+def tag_mask(nw, tag):
+ keys = list(tag_dict.keys())
+ tag_parts = tag.split(',')
+ logger.debug(f'Parsing {tag=} into {len(tag_parts)=}, matching against {len(tag_dict)=}')
+ for part in tag_parts:
+ if part.startswith("-"):
+ keys = [k for k in keys if part[1:] not in k.split('.')]
+ else:
+ keys = [k for k in keys if part in k.split('.')]
+ conditions = []
+ for k in keys:
+ comp = nw.new_node(
+ Nodes.Compare,
+ attrs={'operation': "EQUAL", "data_type": "FLOAT"},
+ input_args=[eval_argument(nw, "MaskTag"), tag_dict[k]]
+ )
+ conditions.append(comp)
+ if len(conditions):
+ return nw.scalar_add(*conditions)
+ else:
+ mask = nw.new_node(Nodes.Value)
+ mask.outputs["Value"].default_value = 1
+ return mask
+
def placement_mask(scale=0.05, select_thresh=0.55, normal_thresh=0.5, normal_thresh_high=2.,
normal_dir=(0, 0, 1), tag=None, return_scalar=False, altitude_range=None):
def selection(nw):
@@ -45,22 +71,10 @@ def selection(nw):
mask = nw.scalar_multiply(mask, facing_mask)
if tag is not None:
- keys = list(tag_dict.keys())
- tag_parts = tag.split(',')
- logger.debug(f'Parsing {tag=} into {len(tag_parts)=}, matching against {len(tag_dict)=}')
- for part in tag_parts:
- if part.startswith("-"):
- keys = [k for k in keys if part[1:] not in k.split('.')]
- else:
- keys = [k for k in keys if part in k.split('.')]
- conditions = []
- for k in keys:
- conditions.append(nw.new_node(Nodes.Compare, attrs={'operation': "EQUAL", "data_type": "FLOAT"}, input_args=[eval_argument(nw, "MaskTag"), tag_dict[k]]))
- if len(conditions) > 0:
- mask = nw.scalar_multiply(
- mask,
- nw.scalar_add(*conditions)
- )
+ mask = nw.scalar_multiply(
+ mask,
+ tag_mask(nw, tag)
+ )
if altitude_range is not None:
z = (nw.new_node(Nodes.SeparateXYZ, [nw.new_node(Nodes.InputPosition)]), 2)
start, end = altitude_range
diff --git a/infinigen/core/placement/detail.py b/infinigen/core/placement/detail.py
index 082522d6c..e4a4c37c4 100644
--- a/infinigen/core/placement/detail.py
+++ b/infinigen/core/placement/detail.py
@@ -78,7 +78,7 @@ def remesh_with_attrs(obj, face_size, apply=True, min_remesh_size=None, attribut
logger.debug(f'remesh_with_attrs on {obj.name=} with {face_size=:.4f} {attributes=}')
temp_copy = deep_clone_obj(obj)
-
+
remesh_size = face_size if min_remesh_size is None else max(face_size, min_remesh_size)
butil.modify_mesh(obj, type='REMESH', apply=True, voxel_size=remesh_size)
@@ -96,7 +96,8 @@ def sharp_remesh_with_attrs(obj, face_size, apply=True, min_remesh_size=None, at
remesh_size = face_size if min_remesh_size is None else max(face_size, min_remesh_size)
butil.modify_mesh(obj, 'REMESH', apply=apply, mode='SHARP',
- octree_depth=int(np.ceil(np.log2((max(obj.dimensions) + .01) / remesh_size))))
+ octree_depth=int(np.ceil(np.log2((max(obj.dimensions) + .01) / remesh_size))),
+ use_remove_disconnected=False)
transfer_attributes.transfer_all(source=temp_copy, target=obj, attributes=attributes, uvs=True)
bpy.data.objects.remove(temp_copy, do_unlink=True)
@@ -113,7 +114,7 @@ def subdivide_to_face_size(obj, from_facesize, to_facesize, apply=True, max_leve
logger.warn(f'subdivide_to_facesize({obj.name=}, {from_facesize=:.6f}, {to_facesize=:.6f}) attempted {levels=}, clamping to {max_levels=}')
levels = max_levels
logger.debug(f'subdivide_to_face_size applying {levels=} of subsurf to {obj.name=}')
- _, mod = butil.modify_mesh(obj, 'SUBSURF', apply=apply,
+ _, mod = butil.modify_mesh(obj, 'SUBSURF', apply=apply,
levels=levels, render_levels=levels, return_mod=True)
return mod # None if apply=True
@@ -142,7 +143,7 @@ def min_max_edgelen(mesh):
def adapt_mesh_resolution(obj, face_size, method, approx=0.2, **kwargs):
-
+
assert obj.type == 'MESH'
assert 0 <= approx and approx <= 0.5
@@ -173,4 +174,4 @@ def adapt_mesh_resolution(obj, face_size, method, approx=0.2, **kwargs):
elif method == 'sharp_remesh':
sharp_remesh_with_attrs(obj, face_size, **kwargs)
else:
- raise ValueError(f'Unrecognized adapt_mesh_resolution(..., {method=})')
\ No newline at end of file
+ raise ValueError(f'Unrecognized adapt_mesh_resolution(..., {method=})')
diff --git a/infinigen/core/placement/factory.py b/infinigen/core/placement/factory.py
index d7d68c888..393ece294 100644
--- a/infinigen/core/placement/factory.py
+++ b/infinigen/core/placement/factory.py
@@ -1,7 +1,8 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
-# Authors:
+# Authors:
# - Alexander Raistrick: AssetFactory, make_asset_collection
# - Lahav Lipson: quickly_resample
@@ -17,13 +18,14 @@
from infinigen.core.util import blender as butil
from infinigen.core.util.math import FixedSeed, int_hash
from . import detail
+from ...assets.utils.object import center
logger = logging.getLogger(__name__)
+
class AssetFactory:
def __init__(self, factory_seed=None, coarse=False):
-
self.factory_seed = factory_seed
if self.factory_seed is None:
self.factory_seed = np.random.randint(1e9)
@@ -51,8 +53,11 @@ def finalize_placeholders(self, placeholders: typing.List[bpy.types.Object]):
def asset_parameters(self, distance: float, vis_distance: float) -> dict:
# Optionally, override to determine the **params input of create_asset w.r.t. camera distance
- return {'face_size': detail.target_face_size(distance), 'distance': distance,
- 'vis_distance': vis_distance}
+ return {
+ 'face_size': detail.target_face_size(distance),
+ 'distance': distance,
+ 'vis_distance': vis_distance
+ }
def create_asset(self, **params) -> bpy.types.Object:
# Override this function to produce a high detail asset
@@ -78,16 +83,20 @@ def spawn_placeholder(self, i, loc, rot):
obj.rotation_euler = rot
else:
logger.debug(f'Not assigning placeholder {obj.name=} location due to presence of'
- 'location-sensitive constraint, typically a follow curve')
+ 'location-sensitive constraint, typically a follow curve')
obj.name = f'{repr(self)}.spawn_placeholder({i})'
if obj.parent is not None:
- logger.warning(f'{obj.name=} has no-none parent {obj.parent.name=}, this may cause it not to get populated')
+ logger.warning(
+ f'{obj.name=} has no-none parent {obj.parent.name=}, this may cause it not to get populated')
return obj
def spawn_asset(self, i, placeholder=None, distance=None, vis_distance=0, loc=(0, 0, 0), rot=(0, 0, 0),
**kwargs):
+
+ if not isinstance(i, int):
+ raise TypeError(f'{i=} {type(i)=}, expected int')
# Not intended to be overridden - override create_asset instead
logger.debug(f'{self}.spawn_asset({i}...)')
@@ -98,13 +107,14 @@ def spawn_asset(self, i, placeholder=None, distance=None, vis_distance=0, loc=(0
if self.coarse:
raise ValueError('Attempted to spawn_asset() on an AssetFactory(coarse=True)')
- if placeholder is None:
+ user_provided_placeholder = placeholder is not None
+
+ if user_provided_placeholder:
+ assert loc == (0, 0, 0) and rot == (0, 0, 0)
+ else:
placeholder = self.spawn_placeholder(i=i, loc=loc, rot=rot)
self.finalize_placeholders([placeholder])
- keep_placeholder = False
- else:
- keep_placeholder = True
- assert loc == (0, 0, 0) and rot == (0, 0, 0)
+
gc_targets = [bpy.data.meshes, bpy.data.textures, bpy.data.node_groups, bpy.data.materials]
@@ -115,7 +125,7 @@ def spawn_asset(self, i, placeholder=None, distance=None, vis_distance=0, loc=(0
obj.name = f'{repr(self)}.spawn_asset({i})'
- if keep_placeholder:
+ if user_provided_placeholder:
if obj is not placeholder:
if obj.parent is None:
butil.parent_to(obj, placeholder, no_inverse=True)
@@ -130,10 +140,12 @@ def spawn_asset(self, i, placeholder=None, distance=None, vis_distance=0, loc=(0
return obj
__call__ = spawn_asset # for convinience
-
-def make_asset_collection(spawn_fns, n, name=None, weights=None, as_list=False, verbose=True, **kwargs):
+ def post_init(self):
+ pass
+def make_asset_collection(spawn_fns, n, name=None, weights=None, as_list=False, verbose=True, centered=False,
+ **kwargs):
if not isinstance(spawn_fns, list):
spawn_fns = [spawn_fns]
if weights is None:
@@ -151,13 +163,16 @@ def make_asset_collection(spawn_fns, n, name=None, weights=None, as_list=False,
for i in r:
fn_idx = np.random.choice(np.arange(len(spawn_fns)), p=weights)
obj = spawn_fns[fn_idx](i=i, **kwargs)
+ if centered:
+ obj.location = -center(obj)
+ butil.apply_transform(obj, True)
objs[fn_idx].append(obj)
-
+
for os, f in zip(objs, spawn_fns):
if hasattr(f, 'finalize_assets'):
f.finalize_assets(os)
- objs = sum(objs, start=[])
+ objs = sum(objs, start=[])
if as_list:
return objs
@@ -166,3 +181,4 @@ def make_asset_collection(spawn_fns, n, name=None, weights=None, as_list=False,
col.hide_viewport = True
col.hide_render = True
return col
+
diff --git a/infinigen/core/placement/instance_scatter.py b/infinigen/core/placement/instance_scatter.py
index 1183be515..ace0c62d9 100644
--- a/infinigen/core/placement/instance_scatter.py
+++ b/infinigen/core/placement/instance_scatter.py
@@ -99,7 +99,7 @@ def geo_instance_scatter(
scaling=Vector((1, 1, 1)), normal=None, normal_fac=1, rotation_offset=None,
selection=True, taper_scale=False, taper_density=False,
ground_offset=0, instance_index=None,
- transform_space='RELATIVE', reset_children=True
+ transform_space='RELATIVE', reset_children=True, realize=False
):
base_geo = nw.new_node(Nodes.ObjectInfo, [base_obj], attrs={'transform_space':transform_space}).outputs['Geometry']
@@ -174,15 +174,16 @@ def geo_instance_scatter(
if ground_offset != 0:
instances = nw.new_node(Nodes.TranslateInstances, [instances],
input_kwargs={ "Translation": nw.combine(0, 0, point_fields['ground_offset']), "Local Space": True})
-
- instances = nw.new_node(Nodes.SetShadeSmooth, input_kwargs={'Geometry': instances, 'Shade Smooth': False})
+
+ if realize:
+ instances = nw.new_node(Nodes.RealizeInstances, [instances])
nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': instances})
def scatter_instances(
collection,
density=None, vol_density=None, max_density=5000,
- scale=None, scale_rand=0, scale_rand_axi=0,
+ scale=None, scale_rand=0, scale_rand_axi=0, apply_geo=False,
**kwargs
):
@@ -217,6 +218,6 @@ def scaling(nw: NodeWrangler):
scatter_obj = butil.spawn_vert(name)
kwargs.update(dict(collection=collection, density=density))
with CountInstance(name):
- surface.add_geomod(scatter_obj, geo_instance_scatter, apply=False, input_kwargs=kwargs)
+ surface.add_geomod(scatter_obj, geo_instance_scatter, apply=apply_geo, input_kwargs=kwargs)
butil.put_in_collection(scatter_obj, butil.get_collection('scatters'))
return scatter_obj
\ No newline at end of file
diff --git a/infinigen/core/placement/path_finding.py b/infinigen/core/placement/path_finding.py
new file mode 100644
index 000000000..0e491d3ca
--- /dev/null
+++ b/infinigen/core/placement/path_finding.py
@@ -0,0 +1,173 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Zeyu Ma
+
+from tqdm import tqdm
+import numpy as np
+import mathutils
+import itertools
+import networkx as nx
+from scipy.sparse import csr_matrix
+import matplotlib.pyplot as plt
+import os
+
+
+def camera_rotation_matrix(pointing_direction, up_vector):
+ forward = pointing_direction / np.linalg.norm(pointing_direction)
+ right = np.cross(forward, up_vector)
+ right /= np.linalg.norm(right)
+ up = np.cross(forward, right)
+ up /= np.linalg.norm(up)
+ return np.column_stack((right, up, forward))
+
+def path_finding(bvhtree, bounding_box, start_pose, end_pose, resolution=100000, margin=0.1):
+ volume = np.product(bounding_box[1] - bounding_box[0])
+ N = np.floor((bounding_box[1] - bounding_box[0]) * (resolution / volume) ** (1/3)).astype(np.int32)
+ NN = np.product(N)
+ # print(f"{N=}")
+ start_location, start_rotation = start_pose
+ end_location, end_rotation = end_pose
+ margin_d = np.ceil((resolution / volume) ** (1/3) * margin)
+ row = []
+ col = []
+ data = []
+
+ def freespace_ray_check(a, b, margin=0):
+ v = b - a
+ location, *_ = bvhtree.ray_cast(a, v, v.length)
+ if location is not None: return False
+ if margin != 0:
+ if v[0] != 0:
+ perp = mathutils.Vector([v[1], -v[0], 0])
+ else:
+ perp = mathutils.Vector([0, v[2], -v[1]])
+ offset = v.cross(perp)
+ offset *= margin / offset.length
+ check_N = 10
+ angle = np.pi * 2 / check_N
+ for i in range(check_N):
+ location, *_ = bvhtree.ray_cast(a + offset, v, v.length)
+ if location is not None: return False
+ tar_direction = offset.cross(v)
+ tar_direction *= margin / tar_direction.length
+ offset = offset * np.cos(angle) + tar_direction * np.sin(angle)
+ return True
+
+ def index(i, j, k):
+ return i * N[1] * N[2] + j * N[2] + k
+
+ x, y, z = np.meshgrid(np.arange(N[0]), np.arange(N[1]), np.arange(N[2]), indexing="ij")
+ x = bounding_box[0][0] + (bounding_box[1][0]-bounding_box[0][0]) * (x+0.5) / N[0]
+ y = bounding_box[0][1] + (bounding_box[1][1]-bounding_box[0][1]) * (y+0.5) / N[1]
+ z = bounding_box[0][2] + (bounding_box[1][2]-bounding_box[0][2]) * (z+0.5) / N[2]
+ x, y, z = x.reshape(-1), y.reshape(-1), z.reshape(-1)
+
+ start_index = index(*np.floor((np.array(start_location) - bounding_box[0]) / (bounding_box[1] - bounding_box[0]) * N).astype(np.int32))
+ end_index = index(*np.floor((np.array(end_location) - bounding_box[0]) / (bounding_box[1] - bounding_box[0]) * N).astype(np.int32))
+ if end_index == start_index: return None
+
+ x[start_index] = start_pose[0].x
+ y[start_index] = start_pose[0].y
+ z[start_index] = start_pose[0].z
+ x[end_index] = end_pose[0].x
+ y[end_index] = end_pose[0].y
+ z[end_index] = end_pose[0].z
+
+ penalty = 99
+ for i, j, k in list(itertools.product(range(N[0]), range(N[1]), range(N[2]))):
+ index_ijk = index(i, j, k)
+ pos_from = mathutils.Vector([x[index_ijk], y[index_ijk], z[index_ijk]])
+ for di, dj, dk in [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 0], [0, 1, 1], [1, 0, 1], [1, -1, 0], [0, 1, -1], [1, 0, -1]]:
+ ni, nj, nk = i+di, j+dj, k+dk
+ if ni >= 0 and nj >= 0 and nk >= 0 and ni < N[0] and nj < N[1] and nk < N[2]:
+ index_nijk = index(ni, nj, nk)
+ pos_to = mathutils.Vector([x[index_nijk], y[index_nijk], z[index_nijk]])
+ connected = freespace_ray_check(pos_from, pos_to)
+ if connected:
+ row.append(index_ijk)
+ col.append(index_nijk)
+ data.append(1 if dk == 0 else penalty)
+ row.append(index_nijk)
+ col.append(index_ijk)
+ data.append(1 if dk == 0 else penalty)
+
+ row = np.array(row)
+ col = np.array(col)
+ data = np.array(data)
+
+ A = csr_matrix((data, (row, col)), shape=(NN, NN))
+ G = nx.from_scipy_sparse_array(A)
+
+ boundaries = []
+ n_neighbors = np.array(A.sum(axis=0))[0]
+
+ for i in range(NN):
+ if n_neighbors[i] != 8 + 10 * penalty:
+ boundaries.append(i)
+
+ lengths_dict = nx.multi_source_dijkstra_path_length(G, boundaries, weight="weight")
+ lengths = np.zeros(NN) + np.inf
+ for n in lengths_dict:
+ lengths[n] = lengths_dict[n]
+
+ mask1 = (lengths[row] >= margin_d)
+ mask2 = (lengths[col] >= margin_d)
+ row = row[mask1 & mask2]
+ col = col[mask1 & mask2]
+ data = data[mask1 & mask2]
+
+ A = csr_matrix((data, (row, col)), shape=(NN, NN))
+ G = nx.from_scipy_sparse_array(A)
+
+
+ try:
+ path = nx.shortest_path(G, start_index, end_index, weight="weight")
+ except:
+ return None
+
+ stack = [start_index]
+
+ for p in path[1:]:
+ back = 0
+ while freespace_ray_check(mathutils.Vector([x[stack[-1-back]], y[stack[-1-back]], z[stack[-1-back]]]), mathutils.Vector([x[p], y[p], z[p]]), margin=margin):
+ back += 1
+ if back == len(stack):
+ break
+ if back != 1:
+ stack = stack[:1-back]
+ stack.append(p)
+
+ locations = []
+ lengths = []
+ for i, p in enumerate(stack):
+ if i == 0:
+ locations.append(start_pose[0])
+ elif i == len(stack) - 1:
+ locations.append(end_pose[0])
+ else:
+ locations.append(mathutils.Vector([x[p], y[p], z[p]]))
+ if len(locations) >= 2: lengths.append((locations[-1] - locations[-2]).length)
+ keyframed_poses = []
+
+ for i in range(len(stack)):
+ if i == 0:
+ keyframed_poses.append((0, *start_pose))
+ else:
+ if i == len(stack) - 1:
+ rotation_euler = end_pose[1]
+ else:
+ rotation_matrix = mathutils.Matrix(camera_rotation_matrix(np.array(locations[i] - locations[i-1]), np.array([0, 0, 1]))) @ mathutils.Matrix([[1, 0, 0], [0, -1, 0], [0, 0, -1]])
+ rotation_euler = rotation_matrix.to_euler()
+ if rotation_euler.y != 0:
+ rotation_euler.y = 0
+ rotation_euler.x += np.pi
+ rotation_euler.z += np.pi
+ angle_differece = [
+ abs(rotation_euler.z - 2 * np.pi - keyframed_poses[i-1][2].z),
+ abs(rotation_euler.z - keyframed_poses[i-1][2].z),
+ abs(rotation_euler.z + 2 * np.pi - keyframed_poses[i-1][2].z),
+ ]
+ rotation_euler.z += (np.argmin(angle_differece) - 1) * 2 * np.pi
+ keyframed_poses.append((np.sum(lengths[:i]), locations[i], rotation_euler))
+ return keyframed_poses
\ No newline at end of file
diff --git a/infinigen/core/placement/placement.py b/infinigen/core/placement/placement.py
index def1b9d12..c76e44a5d 100644
--- a/infinigen/core/placement/placement.py
+++ b/infinigen/core/placement/placement.py
@@ -3,7 +3,6 @@
# Authors: Alexander Raistrick
-
import re
import logging
from collections import defaultdict
@@ -65,7 +64,7 @@ def placeholder_locs(terrain, overall_density, selection, distance_min=0, altitu
return locations
-def points_near_camera(cam, terrain_bvh, n, alt, dist_range):
+def points_near_camera(cam, scene_bvh, n, alt, dist_range):
points = []
while len(points) < n:
@@ -75,7 +74,7 @@ def points_near_camera(cam, terrain_bvh, n, alt, dist_range):
off = rad * mathutils.Vector((np.cos(angle), np.sin(angle), 0))
pos = cam.location + off
- pos, *_ = terrain_bvh.ray_cast(pos, mathutils.Vector((0, 0, -1)))
+ pos, *_ = scene_bvh.ray_cast(pos, mathutils.Vector((0, 0, -1)))
if pos is None:
continue
pos.z += alt
@@ -122,7 +121,7 @@ def get_placeholder_points(obj):
return np.array([obj.matrix_world.translation]).reshape(1, 3)
def parse_asset_name(name):
- match = re.fullmatch('(.*)\((\d+)\)\.spawn_(.*)\((\d+)\)', name)
+ match = re.fullmatch('(.*)\((\d+)\)\..*_(.*)\((\d+)\)', name)
if not match:
return None, None, None, None
return list(match.groups())
@@ -244,7 +243,7 @@ def populate_all(factory_class, camera, dist_cull=200, vis_cull=0, cache_system
return results
-def make_placeholders_float(placeholder_col, terrain_bvh, water):
+def make_placeholders_float(placeholder_col, scene_bvh, water):
deps = bpy.context.evaluated_depsgraph_get()
water_bvh = mathutils.bvhtree.BVHTree.FromObject(water, deps)
@@ -254,7 +253,7 @@ def make_placeholders_float(placeholder_col, terrain_bvh, water):
for p in tqdm(placeholder_col.objects, desc=f'Computing fluid-floating locations for {placeholder_col.name=}'):
w_up, *_ = water_bvh.ray_cast(p.location + margin, up)
if w_up is not None:
- t_up, *_ = terrain_bvh.ray_cast(p.location + margin, up)
+ t_up, *_ = scene_bvh.ray_cast(p.location + margin, up)
z = min(w_up.z, t_up.z) if t_up is not None else w_up.z
z = max(p.location.z, z - 0.7) # the origin will be the creature's foot, allow some space for the rest of it
p.location.z = np.random.uniform(p.location.z, z)
diff --git a/infinigen/core/rendering/post_render.py b/infinigen/core/rendering/post_render.py
index 31cfecf04..dfeb46c51 100644
--- a/infinigen/core/rendering/post_render.py
+++ b/infinigen/core/rendering/post_render.py
@@ -6,17 +6,23 @@
import argparse
import os
+import logging
# ruff: noqa: E402
os.environ["OPENCV_IO_ENABLE_OPENEXR"]="1" # This must be done BEFORE import cv2.
import cv2
import colorsys
+
import numpy as np
from matplotlib import pyplot as plt
from pathlib import Path
from imageio import imwrite
+import flow_vis
+
+logger = logging.getLogger(__name__)
+
def load_exr(path):
assert Path(path).exists() and Path(path).suffix == ".exr", path
return cv2.imread(str(path), cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
@@ -28,7 +34,6 @@ def load_exr(path):
load_uniq_inst = lambda p: load_exr(p).view(np.int32)
def colorize_flow(optical_flow):
- import flow_vis
flow_uv = optical_flow[...,:2]
flow_color = flow_vis.flow_to_color(flow_uv, convert_to_bgr=False)
return flow_color
diff --git a/infinigen/core/rendering/render.py b/infinigen/core/rendering/render.py
index 6b27a2a9f..e248b66ac 100644
--- a/infinigen/core/rendering/render.py
+++ b/infinigen/core/rendering/render.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors:
+# Authors:
# - Lahav Lipson - Render, flat shading, etc
# - Alex Raistrick - Compositing
# - Hei Law - Initial version
@@ -12,6 +12,7 @@
import os
import time
import warnings
+from pathlib import Path
import bpy
import gin
@@ -104,11 +105,11 @@ def compositor_postprocessing(nw, source, show=True, autoexpose=False, autoexpos
if distort > 0:
source = nw.new_node(Nodes.LensDistortion,
input_kwargs={'Image': source, 'Dispersion': distort})
-
+
if color_correct:
source = nw.new_node(Nodes.BrightContrast,
input_kwargs={'Image': source, 'Bright': 1.0, 'Contrast': 4.0})
-
+
if glare:
source = nw.new_node(
Nodes.Glare,
@@ -127,12 +128,12 @@ def compositor_postprocessing(nw, source, show=True, autoexpose=False, autoexpos
@gin.configurable
def configure_compositor_output(
- nw,
- frames_folder,
- image_denoised,
- image_noisy,
- passes_to_save,
- saving_ground_truth,
+ nw,
+ frames_folder,
+ image_denoised,
+ image_noisy,
+ passes_to_save,
+ saving_ground_truth,
):
file_output_node = nw.new_node(Nodes.OutputFile, attrs={
@@ -262,8 +263,12 @@ def postprocess_blendergt_outputs(frames_folder, output_stem):
np.save(flow_dst_path.with_name(f"InstanceSegmentation{output_stem}.npy"), uniq_inst_array)
imwrite(uniq_inst_path.with_name(f"InstanceSegmentation{output_stem}.png"), colorize_int_array(uniq_inst_array))
uniq_inst_path.unlink()
-
-def configure_compositor(frames_folder, passes_to_save, flat_shading):
+
+def configure_compositor(
+ frames_folder: Path,
+ passes_to_save: list,
+ flat_shading: bool,
+):
compositor_node_tree = bpy.context.scene.node_tree
nw = NodeWrangler(compositor_node_tree)
diff --git a/infinigen/core/rendering/resample.py b/infinigen/core/rendering/resample.py
index cf2cf8d1f..c3f098516 100644
--- a/infinigen/core/rendering/resample.py
+++ b/infinigen/core/rendering/resample.py
@@ -1,4 +1,3 @@
-
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
diff --git a/infinigen/core/surface.py b/infinigen/core/surface.py
index 609439762..5b1302f55 100644
--- a/infinigen/core/surface.py
+++ b/infinigen/core/surface.py
@@ -1,10 +1,11 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
-# Authors:
+# Authors:
# - Alex Raistrick: primary author
# - Lahav Lipson: Surface mixing
-
+# - Lingjie Mei: attributes and geo nodes
import string
from collections import defaultdict
@@ -18,9 +19,12 @@
from tqdm import trange
from infinigen.core.util import blender as butil
-from infinigen.core.util.blender import set_geomod_inputs # got moved, left here for import compatibility
-from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes, isnode, infer_output_socket, geometry_node_group_empty_new
+from infinigen.core.util.blender import set_geomod_inputs # got moved, left here for import compatibility
+from infinigen.core.nodes.node_wrangler import NodeWrangler, Nodes, isnode, infer_output_socket, \
+ geometry_node_group_empty_new
from infinigen.core.nodes import node_info
+from infinigen.core import tagging, tags as t
+
def remove_materials(obj):
with butil.SelectObjects(obj):
@@ -40,23 +44,21 @@ def attr_writer(nw, **kwargs):
if data_type is None:
data_type = node_info.NODETYPE_TO_DATATYPE[infer_output_socket(value).type]
- capture = nw.new_node(Nodes.CaptureAttribute,
- attrs={'data_type': data_type},
- input_kwargs={
- 'Geometry': nw.new_node(Nodes.GroupInput),
- 'Value': value
+ capture = nw.new_node(Nodes.CaptureAttribute, attrs={'data_type': data_type},
+ input_kwargs={'Geometry': nw.new_node(Nodes.GroupInput), 'Value': value
})
- output = nw.new_node(Nodes.GroupOutput, input_kwargs={
- 'Geometry': (capture, 'Geometry'),
- name: (capture, 'Attribute')
- })
+ output = nw.new_node(Nodes.GroupOutput,
+ input_kwargs={'Geometry': (capture, 'Geometry'), name: (capture, 'Attribute')
+ })
mod = add_geomod(objs, attr_writer, name=f'write_attribute({name})', apply=apply, attributes=[name])
- return name
+ return name
+
-def read_attr_data(obj, attr, domain='POINT') -> np.array:
+def read_attr_data(obj, attr, domain='POINT', result_dtype=None) -> np.array:
if isinstance(attr, str):
attr = obj.data.attributes[attr]
+ domain = attr.domain
if domain == 'POINT':
n = len(obj.data.vertices)
@@ -66,13 +68,26 @@ def read_attr_data(obj, attr, domain='POINT') -> np.array:
n = len(obj.data.polygons)
else:
raise NotImplementedError
- dtype = attr.data_type
- dim = node_info.DATATYPE_DIMS[dtype]
- field = node_info.DATATYPE_FIELDS[dtype]
- data = np.empty(n * dim)
+ dim = node_info.DATATYPE_DIMS[attr.data_type]
+ field = node_info.DATATYPE_FIELDS[attr.data_type]
+
+ if result_dtype is None:
+ result_dtype = node_info.DATATYPE_TO_PYTYPE[attr.data_type]
+
+ data = np.empty(n * dim, dtype=result_dtype)
attr.data.foreach_get(field, data)
- return data.reshape(-1, dim)
+
+ if dim > 1:
+ data = data.reshape(-1, dim)
+
+ return data
+
+
+def set_active(obj, name):
+ attributes = obj.data.attributes
+ attributes.active_index = next((i for i, a in enumerate(attributes) if a.name == name))
+ attributes.active = attributes[attributes.active_index]
def write_attr_data(obj, attr, data: np.array, type='FLOAT', domain='POINT'):
@@ -85,9 +100,10 @@ def write_attr_data(obj, attr, data: np.array, type='FLOAT', domain='POINT'):
field = node_info.DATATYPE_FIELDS[attr.data_type]
attr.data.foreach_set(field, data.reshape(-1))
+
def new_attr_data(obj, attr, type, domain, data: np.array):
- assert(isinstance(attr, str))
- assert(attr not in obj.data.attributes)
+ assert (isinstance(attr, str))
+ assert (attr not in obj.data.attributes)
obj.data.attributes.new(name=attr, type=type, domain=domain)
attr = obj.data.attributes[attr]
@@ -126,7 +142,8 @@ def attribute_to_vertex_group(obj, attr, name=None, min_thresh=0, binary=False):
if attr_data.shape[-1] != 1:
raise ValueError(
- f'Could not convert non-scalar attribute {attr} to vertex group, expected 1 data dimension but got {attr_data.shape=}')
+ f'Could not convert non-scalar attribute {attr} to vertex group, expected 1 data dimension but '
+ f'got {attr_data.shape=}')
group = obj.vertex_groups.new(name=name)
@@ -180,8 +197,9 @@ def shaderfunc_to_material(shader_func, *args, name=None, **kwargs):
material.node_tree.nodes.remove(material.node_tree.nodes['Principled BSDF']) # remove the default BSDF
nw = NodeWrangler(material.node_tree)
- new_node_tree = shader_func(nw, *args, **kwargs)
+ new_node_tree = shader_func(nw, *args, **kwargs)
+
if new_node_tree is not None:
if isinstance(new_node_tree, tuple) and isnode(new_node_tree[1]):
new_node_tree, volume = new_node_tree
@@ -209,15 +227,18 @@ def add_material(objs, shader_func, selection=None, input_args=None, input_kwarg
if (not reuse) and (name in bpy.data.materials):
name += f"_{seed_generator(8)}"
material = shaderfunc_to_material(shader_func, *input_args, **input_kwargs)
- elif isinstance(selection, str):
+ elif isinstance(selection, (str, t.Semantics)):
+ if isinstance(selection, t.Semantics):
+ selection = selection.value
name = "MixedSurface"
if name in objs[0].data.materials:
material = objs[0].data.materials[name]
else:
material = bpy.data.materials.new(name=name)
material.use_nodes = True
- material.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (1, 0, 1, 1) # Set Magenta
+ material.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (
+ 1, 0, 1, 1) # Set Magenta
objs[0].active_material = material
nw = NodeWrangler(material.node_tree)
@@ -229,7 +250,8 @@ def add_material(objs, shader_func, selection=None, input_args=None, input_kwarg
socket_index_old = 2
else:
socket_index_old = 0
- new_attribute_sum_node = nw.scalar_add((old_attribute_sum_node, socket_index_old), (new_attribute_node, 2))
+ new_attribute_sum_node = nw.scalar_add((old_attribute_sum_node, socket_index_old),
+ (new_attribute_node, 2))
old_attribute_sum_node.name = "Attribute Sum Old"
new_attribute_sum_node.name = "Attribute Sum"
else:
@@ -243,16 +265,14 @@ def add_material(objs, shader_func, selection=None, input_args=None, input_kwarg
socket_index_new = 2
else:
socket_index_new = 0
- selection_weight = nw.divide2(
- (new_attribute_node, 2),
- (new_attribute_sum_node, socket_index_new)
- )
+ selection_weight = nw.divide2((new_attribute_node, 2), (new_attribute_sum_node, socket_index_new))
# spawn in the node tree to mix with it
new_node_tree = shader_func(nw, **input_kwargs)
if new_node_tree is None:
raise ValueError(
- f'{shader_func} returned None while attempting add_material(selection=...). Shaderfunc must return its output to be mixable')
+ f'{shader_func} returned None while attempting add_material(selection=...). Shaderfunc must '
+ f'return its output to be mixable')
if isinstance(new_node_tree, tuple) and isnode(new_node_tree[1]):
new_node_tree, volume = new_node_tree
nw.new_node(Nodes.MaterialOutput, input_kwargs={'Volume': volume})
@@ -267,11 +287,9 @@ def add_material(objs, shader_func, selection=None, input_args=None, input_kwarg
obj.active_material = material
return material
-def add_geomod(objs, geo_func,
- name=None, apply=False, reuse=False, input_args=None,
- input_kwargs=None, attributes=None, show_viewport=True, selection=None,
- domains=None, input_attributes=None, ):
-
+
+def add_geomod(objs, geo_func, name=None, apply=False, reuse=False, input_args=None, input_kwargs=None,
+ attributes=None, show_viewport=True, selection=None, domains=None, input_attributes=None, ):
if input_args is None:
input_args = []
if input_kwargs is None:
@@ -295,7 +313,6 @@ def add_geomod(objs, geo_func,
ng = None
for obj in objs:
-
mod = obj.modifiers.new(name=name, type='NODES')
mod.show_viewport = False
@@ -323,7 +340,8 @@ def add_geomod(objs, geo_func,
identifiers = [outputs[i].identifier for i in range(len(outputs)) if outputs[i].type != 'GEOMETRY']
if len(identifiers) != len(attributes):
raise Exception(
- f"has {len(identifiers)} identifiers, but {len(attributes)} attributes. Specifically, {identifiers=} and {attributes=}")
+ f"has {len(identifiers)} identifiers, but {len(attributes)} attributes. Specifically, "
+ f"{identifiers=} and {attributes=}")
for id, att_name in zip(identifiers, attributes):
# attributes are a 1-indexed list, and Geometry is the first element, so we start from 2
# while f'Output_{i}_attribute_name' not in
@@ -364,12 +382,9 @@ def __init__(self):
def get_surface(name):
if name == '':
return NoApply
-
- prefixes = [
- 'infinigen.infinigen_gpl.surfaces',
- 'infinigen.assets.materials',
- 'infinigen.assets.scatters'
- ]
+
+ prefixes = ['infinigen.infinigen_gpl.surfaces', 'infinigen.assets.materials',
+ 'infinigen.assets.scatters']
for prefix in prefixes:
try:
return importlib.import_module('.' + name, prefix)
@@ -385,7 +400,6 @@ def sample_registry(registry):
@gin.configurable('registry')
def initialize_from_gin(self, smooth_categories=0, **gin_category_info):
-
if smooth_categories != 0:
raise NotImplementedError
@@ -398,14 +412,14 @@ def __call__(self, category_key):
if self._registry is None:
raise ValueError(
'Surface registry has not been initialized! Have you loaded gin and called .initialize()?'
- 'Note, this step cannot happen at module initialization time, as gin is not yet loaded'
- )
+ 'Note, this step cannot happen at module initialization time, as gin is not yet loaded')
if category_key not in self._registry:
raise KeyError(
- f'registry recieved request with {category_key=}, but no gin_config for this key was provided. {self._registry.keys()=}')
+ f'registry recieved request with {category_key=}, but no gin_config for this key was '
+ f'provided. {self._registry.keys()=}')
- return self.sample_registry(self._registry[category_key])
+ return self.sample_registry(self._registry[category_key])
registry = Registry()
diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py
new file mode 100644
index 000000000..1ec08b5cd
--- /dev/null
+++ b/infinigen/core/tagging.py
@@ -0,0 +1,428 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Yihan Wang, Karhan Kayan: face based tagging, canonical surface tagging, mask extraction
+
+
+import os
+import bpy
+import json
+import logging
+
+import numpy as np
+import infinigen.core.util.blender as butil
+from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler
+from infinigen.core import surface
+
+from . import tags as t
+
+from typing import Union, Any
+
+logger = logging.getLogger(__name__)
+
+PREFIX = 'TAG_'
+COMBINED_ATTR_NAME = 'MaskTag'
+
+
+class AutoTag():
+ tag_dict = {}
+
+ def __init__(self):
+ self.tag_dict = {}
+
+ def clear(self):
+ self.tag_dict = {}
+
+ # This function now only supports APPLIED OBJECTS
+ # PLEASE KEEP ALL THE GEOMETRY APPLIED BEFORE SCATTERING THEM ON THE TERRAIN
+ # PLEASE DO NOT USE BOOLEAN TAGS FOR OTHER USE
+ def save_tag(self, path='./MaskTag.json'):
+ with open(path, 'w') as f:
+ json.dump(self.tag_dict, f)
+
+ def load_tag(self, path='./MaskTag.json'):
+ with open(path, 'r') as f:
+ self.tag_dict = json.load(f)
+
+ def _extract_incoming_tagmasks(self, obj):
+
+ new_attr_names = [
+ name for name in obj.data.attributes.keys()
+ if name.startswith(PREFIX)
+ ]
+
+ n_poly = len(obj.data.polygons)
+ for name in new_attr_names:
+ attr = obj.data.attributes[name]
+ if attr.domain != 'FACE':
+ raise ValueError(f'Incoming attribute {obj.name=} {attr.name=} had invalid {attr.domain=}, expected FACE')
+ if len(attr.data) != n_poly:
+ raise ValueError(f'Incoming attribute {obj.name=} {attr.name=} had invalid {len(attr.data)=}, expected {n_poly=}')
+
+ new_attrs = {
+ name[len(PREFIX):]: surface.read_attr_data(obj, name, 'FACE')
+ for name in new_attr_names
+ }
+
+ for name, vals in new_attrs.items():
+ if vals.dtype == bool:
+ continue
+ elif vals.dtype.kind == 'f':
+ new_attrs[name] = vals > 0.5
+ elif vals.dtype.kind == 'i':
+ new_attrs[name] = vals > 0
+ else:
+ raise ValueError(f'Incoming attribute {obj.name=} had invalid np dtype {vals.dtype} {vals.dtype.kind=}, expected float or ideally boolean ')
+
+
+ for name, arr in new_attrs.items():
+ if arr.dtype != bool:
+ raise ValueError(f'Retrieved incoming tag mask {name=} had {arr.dtype=}, expected bool')
+
+ for name in new_attr_names:
+ obj.data.attributes.remove(obj.data.attributes[name])
+
+ return new_attrs
+
+ def _specialize_tag_name(self, vi, name, tag_name_lookup):
+
+ if '.' in name:
+ raise ValueError(f'{name=} should not contain separator character "."')
+
+ if vi == 0:
+ return name
+
+ existing = tag_name_lookup[vi - 1]
+ parts = set(existing.split('.'))
+
+ if name in parts:
+ return existing
+
+ parts.add(name)
+ return '.'.join(sorted(list(parts)))
+
+ def _relabel_obj_single(self, obj, tag_name_lookup):
+
+ n_poly = len(obj.data.polygons)
+ new_attrs = self._extract_incoming_tagmasks(obj)
+
+ if COMBINED_ATTR_NAME in obj.data.attributes.keys():
+ domain = obj.data.attributes[COMBINED_ATTR_NAME].domain
+ if domain != 'FACE':
+ raise ValueError(f'{obj.name=} had {COMBINED_ATTR_NAME} on {domain=}, expected FACE')
+ tagint = surface.read_attr_data(obj, COMBINED_ATTR_NAME, domain='FACE')
+ else:
+ tagint = np.full(n_poly, 0, np.int64)
+
+ assert tagint.dtype == np.int64, tagint.dtype
+
+ for name, new_mask in new_attrs.items():
+
+ affected_tagints = np.unique(tagint[new_mask])
+
+ for vi in affected_tagints:
+
+ affected_mask = new_mask * (tagint == vi)
+ if not affected_mask.any():
+ continue
+
+ new_tag_name = self._specialize_tag_name(vi, name, tag_name_lookup)
+ tag_value = self.tag_dict.get(new_tag_name)
+
+ if tag_value is None:
+ tag_value = len(self.tag_dict) + 1
+ self.tag_dict[new_tag_name] = tag_value
+ tag_name_lookup.append(new_tag_name)
+
+ assert len(self.tag_dict) == len(tag_name_lookup), \
+ f'{len(self.tag_dict)=} yet {len(tag_name_lookup)=}, out of sync at {vi=} {new_tag_name=}'
+ assert new_tag_name in tag_name_lookup
+
+ logger.debug(f"{self._relabel_obj_single.__name__} updating {vi=} to {new_tag_name=} with {affected_mask.mean()=:.2f} for {obj.name=}")
+
+ tagint[affected_mask] = tag_value
+
+ if COMBINED_ATTR_NAME not in obj.data.attributes.keys():
+ mask_tag_attr = obj.data.attributes.new(COMBINED_ATTR_NAME, 'INT', 'FACE')
+ else:
+ mask_tag_attr = obj.data.attributes[COMBINED_ATTR_NAME]
+
+ mask_tag_attr.data.foreach_set('value', tagint)
+
+
+ def relabel_obj(self, root_obj):
+
+ tag_name_lookup = [None] * len(self.tag_dict)
+
+ for name, tag_id in self.tag_dict.items():
+ key = tag_id - 1
+ if key >= len(tag_name_lookup):
+ raise IndexError(f'{name} had {tag_id=} {key=} yet {len(self.tag_dict)=}')
+ if tag_name_lookup[key] is not None:
+ raise ValueError(f'{name=} {tag_id=} {key=} attempted to overwrite {tag_name_lookup[key]=}')
+ tag_name_lookup[key] = name
+
+ for obj in butil.iter_object_tree(root_obj):
+ if obj.type != 'MESH':
+ continue
+ self._relabel_obj_single(obj, tag_name_lookup)
+
+ return root_obj
+
+tag_system = AutoTag()
+
+def print_segments_summary(obj: bpy.types.Object):
+
+ tagint = surface.read_attr_data(obj, COMBINED_ATTR_NAME, domain='FACE')
+
+ results = []
+ for vi in np.unique(tagint):
+ mask = (tagint == vi)
+ results.append((vi, mask.mean()))
+
+ results.sort(key=lambda x: x[1], reverse=True)
+
+ print(f'Tag Segments Summary for {obj.name=}')
+ for vi, mean in results:
+ name = _name_for_tagval(vi)
+ print(f' {mean*100:.1f}% {vi=} {name}')
+
+def tag_object(obj, name=None, mask=None):
+
+ if name is not None:
+ name = t.to_string(name)
+
+ for o in butil.iter_object_tree(obj):
+
+ if o.type != 'MESH':
+ continue
+
+ if name is not None:
+
+ n_poly = len(o.data.polygons)
+
+ if n_poly == 0:
+ logger.debug(f'{tag_object.__name__} had {n_poly=} for {o.name=} {name=} child of {obj.name=}')
+ continue
+
+ mask_o = np.full(n_poly, 1, dtype=bool) if mask is None else mask
+
+ assert isinstance(mask_o, np.ndarray)
+ assert len(mask_o) == n_poly
+
+ logger.debug(f'{tag_object.__name__} applying {name=} {mask_o.mean()=:.2f} to {o.name=}')
+ surface.write_attr_data(
+ obj=o,
+ attr=(PREFIX + name),
+ data=mask_o,
+ type='BOOLEAN',
+ domain='FACE'
+ )
+
+ tag_system.relabel_obj(obj)
+
+def vert_mask_to_tri_mask(obj, vert_mask, require_all=True):
+
+ arr = np.zeros(len(obj.data.polygons) * 3)
+ obj.data.polygons.foreach_get('vertices', arr)
+ face_vert_idxs = arr.reshape(-1, 3).astype(int)
+
+ if require_all:
+ return (
+ vert_mask[face_vert_idxs[:, 0]] *
+ vert_mask[face_vert_idxs[:, 1]] *
+ vert_mask[face_vert_idxs[:, 2]]
+ )
+ else:
+ return (
+ vert_mask[face_vert_idxs[:, 0]] |
+ vert_mask[face_vert_idxs[:, 1]] |
+ vert_mask[face_vert_idxs[:, 2]]
+ )
+
+CANONICAL_TAGS = [t.Subpart.Back, t.Subpart.Front, t.Subpart.Top, t.Subpart.Bottom]
+CANONICAL_TAG_MEANINGS = {
+ t.Subpart.Back: (np.min, 0),
+ t.Subpart.Front: (np.max, 0),
+ t.Subpart.Bottom: (np.min, 2),
+ t.Subpart.Top: (np.max, 2),
+}
+
+def tag_canonical_surfaces(obj, rtol=0.01):
+
+ obj.update_from_editmode()
+
+ n_vert = len(obj.data.vertices)
+ n_poly = len(obj.data.polygons)
+
+ verts = np.empty(n_vert * 3, dtype=float)
+ obj.data.vertices.foreach_get('co', verts)
+ verts = verts.reshape(n_vert, 3)
+
+ for tag in CANONICAL_TAGS:
+
+ gather_func, axis_idx = CANONICAL_TAG_MEANINGS[tag]
+ target_axis_val = gather_func(verts[:, axis_idx])
+
+ atol = rtol * obj.dimensions[axis_idx]
+ vert_mask = np.isclose(verts[:, axis_idx], target_axis_val, atol=atol)
+
+ face_mask = vert_mask_to_tri_mask(obj, vert_mask, require_all=True)
+
+ if not face_mask.any():
+ logger.warning(f'{tag_canonical_surfaces.__name__} found got {face_mask.mean()=:.2f} for {tag=} on {obj.name=}')
+
+ logger.debug(f'{tag_canonical_surfaces.__name__} applying {tag=} {face_mask.mean()=:.2f} to {obj.name=}')
+ surface.write_attr_data(obj, PREFIX + tag.value, face_mask, type='BOOLEAN', domain='FACE')
+
+ tag_system.relabel_obj(obj)
+
+def tag_nodegroup(nw: NodeWrangler, input_node, name: t.Tag, selection=None):
+
+ name = PREFIX + t.to_string(name)
+ sel = surface.eval_argument(nw, selection)
+ store_named_attribute = nw.new_node(
+ Nodes.StoreNamedAttribute,
+ input_kwargs={
+ 'Geometry': input_node,
+ 'Name': name,
+ 'Selection': sel,
+ 'Value': True
+ },
+ attrs={
+ 'domain': 'FACE',
+ 'data_type': 'BOOLEAN'
+ }
+ )
+ return store_named_attribute
+
+def _name_for_tagval(i: int) -> str | None:
+
+ if i == 0:
+ # index 0 represents an untagged face
+ return None
+
+ name = next(
+ (k for k, v in tag_system.tag_dict.items() if v == i),
+ None
+ )
+
+ if name is None:
+ raise ValueError(f'Found {name=} for {i=} in {tag_system.tag_dict=}')
+
+ return name
+
+def union_object_tags(obj):
+
+ if COMBINED_ATTR_NAME not in obj.data.attributes:
+ return set()
+
+ masktag = surface.read_attr_data(obj, COMBINED_ATTR_NAME)
+ res = set()
+ for v in np.unique(masktag):
+ if v == 0:
+ continue
+ res = res.union(_name_for_tagval(v).split('.'))
+
+ def try_convert(x):
+ try:
+ return t.to_tag(x)
+ except ValueError:
+ return x
+
+ return {try_convert(x) for x in res}
+
+def tagged_face_mask(obj: bpy.types.Object, tags: Union[t.Subpart]) -> np.ndarray:
+
+ # ASSUMES: object is triangulated, no quads/polygons
+
+ tags = t.to_tag_set(tags)
+ pos_tags = [t.to_string(tagval) for tagval in tags if not isinstance(tagval, t.Negated)]
+ neg_tags = [t.to_string(tagval.tag) for tagval in tags if isinstance(tagval, t.Negated)]
+ del tags
+
+ n_poly = len(obj.data.polygons)
+ if COMBINED_ATTR_NAME not in obj.data.attributes:
+ return np.ones(n_poly, dtype=bool)
+ masktag = surface.read_attr_data(obj, COMBINED_ATTR_NAME, domain='FACE')
+ face_mask = np.zeros(n_poly, dtype=bool)
+
+ for v in np.unique(masktag):
+
+ if v == 0:
+ name_parts = []
+ else:
+ name_parts = _name_for_tagval(v).split('.')
+
+ v_mask = (masktag == v)
+
+ if len(pos_tags) > 0 and not all(tag in name_parts for tag in pos_tags):
+ continue
+ if len(neg_tags) > 0 and any(tag in name_parts for tag in neg_tags):
+ continue
+
+ face_mask |= v_mask
+
+ logger.debug(f'{obj.name=} had {face_mask.mean()=:.2f} for {pos_tags=} {neg_tags=}')
+
+ return face_mask
+
+def extract_tagged_faces(obj: bpy.types.Object, tags: set, nonempty=False) -> bpy.types.Object:
+ "extract the surface that satisfies all tags"
+
+ # Ensure we're dealing with a mesh object
+ if obj.type != 'MESH':
+ raise TypeError("Object is not a mesh!")
+
+ face_mask = tagged_face_mask(obj, tags)
+
+ if nonempty and not face_mask.any():
+ raise ValueError(f'extract_tagged_faces({obj.name=}, {tags=}, {nonempty=}) got empty mask for {len(obj.data.polygons)}')
+
+ return extract_mask(obj, face_mask, nonempty=nonempty)
+
+def extract_mask(
+ obj: bpy.types.Object,
+ face_mask: np.array,
+ nonempty=False
+) -> bpy.types.Object:
+
+ if not face_mask.any():
+ if nonempty:
+ raise ValueError(f'extract_mask({obj.name=}) got empty mask')
+ return butil.spawn_vert()
+
+ orig_hide_viewport = obj.hide_viewport
+ obj.hide_viewport = False
+
+ # Switch to Edit mode, duplicate the selection, and separate it
+ with butil.SelectObjects(obj, active=0):
+
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ for poly in obj.data.polygons:
+ poly.select = face_mask[poly.index]
+ if nonempty and len([p for p in obj.data.polygons if p.select]) == 0:
+ raise ValueError(f'extract_mask({obj.name=}, {nonempty=}) failed to select polygons')
+
+ with butil.ViewportMode(obj, 'EDIT'):
+ bpy.ops.mesh.duplicate_move()
+ bpy.ops.mesh.separate(type='SELECTED')
+
+ res = next((o for o in bpy.context.selected_objects if o != obj), None)
+
+ obj.hide_viewport = orig_hide_viewport
+
+ if nonempty:
+ if res is None:
+ raise ValueError(f'extract_mask({obj.name=}) got {res=} for {face_mask.mean()=}')
+ if len(res.data.polygons) == 0:
+ raise ValueError(f'extract_mask({obj.name=}) got {res=} with {len(res.data.polygons)=}')
+ elif res is None:
+ logger.warning(f'extract_mask({obj.name=}) failed to extract any faces')
+ return butil.spawn_vert()
+
+ return res
\ No newline at end of file
diff --git a/infinigen/core/tags.py b/infinigen/core/tags.py
new file mode 100644
index 000000000..f31d38f6d
--- /dev/null
+++ b/infinigen/core/tags.py
@@ -0,0 +1,306 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from __future__ import annotations
+
+from abc import ABCMeta
+from enum import Enum, EnumMeta
+from dataclasses import dataclass
+
+class ABCEnumMeta(EnumMeta, ABCMeta):
+ pass
+
+class Tag:
+
+ def __neg__(self) -> Negated:
+ return Negated(self)
+
+class StringTag(Tag):
+
+ def __init__(self, desc: str):
+ self.desc = desc
+
+class EnumTag(Tag, Enum, metaclass=ABCEnumMeta):
+ pass
+
+class Semantics(EnumTag):
+
+ # Mesh types
+ Room = "room"
+ Object = "object"
+ Cutter = "cutter"
+
+ # Room types
+ Kitchen = "kitchen"
+ Bedroom = 'bedroom'
+ LivingRoom = 'living-room'
+ Closet = 'closet'
+ Hallway = 'hallway'
+ Bathroom = 'bathroom'
+ Garage = 'garage'
+ Balcony = 'balcony'
+ DiningRoom = 'dining-room'
+ Utility = 'utility'
+ Staircase = 'staircase'
+
+ # Object types
+ Furniture = "furniture"
+ FloorMat = "FloorMat"
+ WallDecoration = "wall-decoration"
+ HandheldItem = "handheld-item"
+
+ # Furniture functions
+ Storage = "storage"
+ Seating = "seating"
+ LoungeSeating = "lounge-seating"
+ Table = "table"
+ Bathing = "bathing"
+ SideTable = "side-table"
+ Watchable = "watchable"
+ Desk = "desk"
+ Bed = "bed"
+ Sink = "sink"
+ CeilingLight = "ceiling-light"
+ Lighting = "lighting"
+ KitchenCounter = "kitchen-counter"
+ KitchenAppliance = "kitchen-appliance"
+
+ # Small Object Functions
+ TableDisplayItem = "table-display-item"
+ OfficeShelfItem = "office-shelf-item"
+ KitchenCounterItem = "kitchen-counter-item"
+ FoodPantryItem = "food-pantry"
+ BathroomItem = "bathroom-item"
+ ShelfTrinket = "shelf-trinket"
+ Dishware = "dishware"
+ Cookware = "cookware"
+ Utensils = "utensils"
+ ClothDrapeItem = "cloth-drape"
+
+ # Object Access Type
+ AccessTop = "access-top"
+ AccessFront = "access-front"
+ AccessAnySide = "access-any-side"
+ AccessAllSides = "access-all-sides"
+
+ # Object Access Method
+ AccessStandingNear = "access-stand-near"
+ AccessSit = "access-stand-near"
+ AccessOpenDoor = "access-open-door"
+ AccessHand = "access-with-hand"
+
+ # Special Case Objects
+ Chair = "chair"
+ Window = 'window'
+ Open = 'open'
+ Entrance = 'entrance'
+ Door = 'door'
+ StaircaseWall = 'staircase-wall'
+
+ # Solver feature flags
+ # TODO these should not be in Semantics
+ RealPlaceholder = "real-placeholder"
+ AssetAsPlaceholder = "asset-as-placeholder"
+ AssetPlaceholderForChildren = "asset-placeholder-for-children"
+ PlaceholderBBox = 'placeholder-bbox'
+ SingleGenerator = 'single-generator'
+ NoRotation = 'no-rotation'
+ NoCollision = 'no-collision'
+ NoChildren = 'no-children'
+
+ def __str__(self):
+ return f'{self.__class__.__name__}({self.value})'
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}.{self.name}'
+
+class Subpart(EnumTag):
+ SupportSurface = "support"
+ Interior = "interior"
+ Exterior = "exterior"
+ Visible = "visible"
+ Invisible = "invisible"
+ Bottom = "bottom"
+ Top = "top"
+ Side = "side"
+ Back = "back"
+ Front = "front"
+ Ceiling = "ceiling"
+ Wall = "wall"
+
+ StaircaseWall = "staircase-wall" # TODO Lingjie Remove
+
+ def __str__(self):
+ return f'{self.__class__.__name__}({self.value})'
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}.{self.name}'
+
+@dataclass(frozen=True)
+class FromGenerator(Tag):
+ generator: type
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({self.generator.__name__})'
+
+@dataclass(frozen=True)
+class Negated(Tag):
+ tag: Tag
+
+ def __str__(self):
+ return "-" + str(self.tag)
+
+ def __repr__(self):
+ return f"-{repr(self.tag)}"
+
+ def __neg__(self):
+ return self.tag
+
+ def __post_init__(self):
+ assert not isinstance(self.tag, Negated), "dont construct double negative tags"
+
+@dataclass(frozen=True)
+class Variable(Tag):
+ name: str
+
+ def __post_init__(self):
+ assert isinstance(self.name, str)
+
+ def __repr__(self):
+ return f'{self.__class__.__name__}({self.name})'
+
+ def __str__(self):
+ return self.name
+
+@dataclass(frozen=True)
+class SpecificObject(Tag):
+ name: str
+
+def decompose_tags(tags: set[Tag]):
+
+ positive, negative = set(), set()
+
+ for t in tags:
+ match t:
+ case Negated(tag):
+ negative.add(tag)
+ case _:
+ positive.add(t)
+
+ return positive, negative
+
+def contradiction(tags: set[Tag]):
+
+ pos, neg = decompose_tags(tags)
+
+ if pos.intersection(neg):
+ return True
+
+ if len([t for t in pos if isinstance(t, FromGenerator)]) > 1:
+ return True
+ if len([t for t in tags if isinstance(t, SpecificObject | Variable)]) > 1:
+ return True
+
+ return False
+
+def implies(t1: set[Tag], t2: set[Tag]):
+
+ p1, n1 = decompose_tags(t1)
+ p2, n2 = decompose_tags(t2)
+
+ return (
+ not contradiction(t1)
+ and p1.issuperset(p2)
+ and n1.issuperset(n2)
+ )
+
+def satisfies(t1: set[Tag], t2: set[Tag]):
+
+ p1, n1 = decompose_tags(t1)
+ p2, n2 = decompose_tags(t2)
+
+ return (
+ p1.issuperset(p2)
+ and not n1.intersection(p2)
+ and not n2.intersection(p1)
+ )
+
+def difference(t1: set[Tag], t2: set[Tag]):
+
+ """Return a set of predicates representing the difference
+
+ If the difference is empty, will return a contradictory set of predicates.
+ """
+
+ p1, n1 = decompose_tags(t1)
+ p2, n2 = decompose_tags(t2)
+
+ pos = p1.union(n2 - n1)
+ neg = n1.union(p2 - p1)
+
+ return pos.union(Negated(n) for n in neg)
+
+def to_tag(s: str | Tag | type, fac_context=None) -> Tag:
+
+ if isinstance(s, Tag):
+ return s
+
+ if type(s) is type:
+ if not fac_context:
+ raise ValueError(f"to_tag got {s=} but {fac_context=}")
+ if s not in fac_context:
+ raise ValueError(f"Got {s=} of type=type but it was not in fac_context")
+ return FromGenerator(s)
+
+ assert isinstance(s, str), s
+
+ if s.startswith("-"):
+ return Negated(to_tag(s[1:]))
+
+ if fac_context is not None:
+ fac = next((f for f in fac_context.keys() if f.__name__ == s), None)
+ if fac:
+ return FromGenerator(fac)
+
+ s = s.strip("\"\'")
+
+ try:
+ return Semantics[s]
+ except KeyError:
+ pass
+
+ try:
+ return Subpart[s]
+ except KeyError:
+ pass
+
+ raise ValueError(f"to_tag got {s=} but could not resolve it. Please see tags.Semantics and tags.Subpart for available tag strings")
+
+def to_string(tag: Tag | str):
+
+ if isinstance(tag, str):
+ return tag
+
+ match tag:
+ case Semantics() | Subpart():
+ return tag.value
+ case StringTag():
+ return tag.desc
+ case FromGenerator():
+ return tag.__name__
+ case Negated():
+ raise ValueError(f'Negated tag {tag=} is not allowed here')
+ case _:
+ raise ValueError(f'to_string unhandled {tag=}')
+
+def to_tag_set(x, fac_context=None):
+ match x:
+ case None:
+ return set()
+ case set() | list() | tuple() | frozenset():
+ return {to_tag(xi, fac_context=fac_context) for xi in x}
+ case x:
+ return {to_tag(x, fac_context=fac_context)}
\ No newline at end of file
diff --git a/infinigen/core/util/bevelling.py b/infinigen/core/util/bevelling.py
new file mode 100644
index 000000000..c2a50372e
--- /dev/null
+++ b/infinigen/core/util/bevelling.py
@@ -0,0 +1,79 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Zeyu Ma
+
+import bpy
+import mathutils
+import bmesh
+import numpy as np
+
+from infinigen.core.nodes.node_wrangler import Nodes
+from .blender import ViewportMode
+
+
+def special_bounds(obj):
+ inf = 1e5
+ points = []
+ for v in obj.data.vertices:
+ points.append(v.co)
+ points = np.array(points)
+ mask = np.sum(points ** 2, axis=-1) ** 0.5 < 0.5 * inf
+ return points[mask].min(axis=0), points[mask].max(axis=0)
+
+def on_bound_edges(points, points_min, points_max):
+ flags = [0, 0, 0]
+ eps = 1e-4
+ for i in range(3):
+ if abs(points[i] - points_min[i]) < eps:
+ flags[i] = -1
+ elif abs(points[i] - points_max[i]) < eps:
+ flags[i] = 1
+ return flags
+
+def get_bevel_edges(obj):
+ inf = 1e5
+ points_min, points_max = special_bounds(obj)
+ bm = bmesh.new()
+ bm.from_mesh(obj.data)
+ edges = []
+ for edge in bm.edges:
+ on_bounds_flag = [0, 0, 0]
+ flags = []
+ mags = []
+ for i in range(2):
+ pos = np.array([edge.verts[i].co.x, edge.verts[i].co.y, edge.verts[i].co.z])
+ flags.append(on_bound_edges(pos, points_min, points_max))
+ mags.append(np.sum(pos ** 2) ** 0.5)
+ for j in range(3):
+ on_bounds_flag[j] = flags[0][j] != 0 and flags[0][j] == flags[1][j]
+ if np.sum(on_bounds_flag) >= 2:
+ edges.append(edge.index)
+ elif mags[0] > 0.5 * inf and mags[0] < 1.5 * inf:
+ edges.append(edge.index)
+ return edges
+
+def add_bevel(obj, edges, offset=0.03, segments=8):
+ with ViewportMode(obj, mode='EDIT'):
+ bpy.ops.mesh.select_mode(type="EDGE")
+ bpy.ops.mesh.select_all(action = 'DESELECT')
+ bm = bmesh.from_edit_mesh(obj.data)
+ for edge in bm.edges:
+ if edge.index in edges:
+ edge.select_set(True)
+ bpy.ops.mesh.bevel(offset=offset, offset_pct=0, segments=segments, release_confirm=True)
+ return obj
+
+def complete_bevel(nw, geometry, preprocess):
+ inf = 1e5
+ geometry = nw.new_node(Nodes.RealizeInstances, [geometry])
+ if not preprocess:
+ return geometry
+ return nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': (geometry, 0), 'Offset': nw.new_node(Nodes.Vector, attrs={"vector": mathutils.Vector((inf, 0, 0))})})
+
+def complete_no_bevel(nw, geometry, preprocess):
+ inf = 1e5
+ geometry = nw.new_node(Nodes.RealizeInstances, [geometry])
+ if not preprocess:
+ return geometry
+ return nw.new_node(Nodes.SetPosition, input_kwargs={'Geometry': (geometry, 0), 'Offset': nw.new_node(Nodes.Vector, attrs={"vector": mathutils.Vector((2 * inf, 0, 0))})})
diff --git a/infinigen/core/util/blender.py b/infinigen/core/util/blender.py
index f339fe006..6c89d854c 100644
--- a/infinigen/core/util/blender.py
+++ b/infinigen/core/util/blender.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Alex Raistrick, Zeyu Ma, Lahav Lipson, Hei Law, Lingjie Mei
+# Authors: Alex Raistrick, Zeyu Ma, Lahav Lipson, Hei Law, Lingjie Mei, Karhan Kayan
from collections import defaultdict
@@ -44,12 +44,14 @@ def deep_clone_obj(obj, keep_modifiers=False, keep_materials=False):
new_obj.data.materials.pop()
bpy.context.collection.objects.link(new_obj)
return new_obj
-
+
+copy = deep_clone_obj
+
def get_all_bpy_data_targets():
D = bpy.data
return [
D.objects, D.collections, D.movieclips, D.particles,
- D.meshes, D.curves, D.armatures, D.node_groups
+ D.meshes, D.curves, D.armatures, D.node_groups,
]
class ViewportMode:
@@ -93,17 +95,64 @@ def __init__(self, objects, active=0):
self.saved_objs = None
self.saved_active = None
+ def _check_selectable(self):
+ unlinked = [
+ o for o in self.objects
+ if o.name not in bpy.context.scene.objects
+ ]
+ if len(unlinked) > 0:
+ raise ValueError(f'{SelectObjects.__name__} had objects {unlinked=} which are not in bpy.context.scene.objects and cannot be selected')
+
+ hidden = [
+ o for o in self.objects
+ if o.hide_viewport
+ ]
+ if len(hidden) > 0:
+ raise ValueError(f'{SelectObjects.__name__} had objects {hidden=} which are hidden and cannot be selected')
+
+ def _get_intended_active(self):
+ if isinstance(self.active, int):
+ if self.active >= len(self.objects):
+ return None
+ else:
+ return self.objects[self.active]
+ else:
+ return self.active
+
+ def _validate(self, error=False):
+
+ if error:
+ def msg(str):
+ raise ValueError(str)
+ else:
+ msg = logger.warning
+
+ difference = set(self.objects) - set(bpy.context.selected_objects)
+ if len(difference):
+ msg(
+ f"{SelectObjects.__name__} failed to select {self.objects=}, result was {bpy.context.selected_objects=}. "
+ "The most common cause is that the objects are in a collection with col.hide_viewport=True"
+ )
+
+
+ intended = self._get_intended_active()
+ if intended is not None and bpy.context.active_object != intended:
+ msg(
+ f"{SelectObjects.__name__} failed to set active object to {intended=}, result was {bpy.context.active_object=}"
+ )
+
def __enter__(self):
self.saved_objects = list(bpy.context.selected_objects)
self.saved_active = bpy.context.active_object
+
select_none()
select(self.objects)
- if len(self.objects):
- if isinstance(self.active, int):
- bpy.context.view_layer.objects.active = self.objects[self.active]
- else:
- bpy.context.view_layer.objects.active = self.active
+ intended = self._get_intended_active()
+ if intended is not None:
+ bpy.context.view_layer.objects.active = intended
+
+ self._validate()
def __exit__(self, *_):
@@ -113,7 +162,7 @@ def enforce_not_deleted(o):
return o if o.name in bpy.data.objects else None
except ReferenceError:
return None
-
+
self.saved_objects = [enforce_not_deleted(o) for o in self.saved_objects]
self.saved_objects = [o for o in self.saved_objects if o is not None]
@@ -213,24 +262,31 @@ def select_none():
obj.select_set(False)
-def select(objs):
+def select(objs: bpy.types.Object | list[bpy.types.Object]):
select_none()
if not isinstance(objs, list):
objs = [objs]
for o in objs:
+ if o.name not in bpy.context.scene.objects:
+ raise ValueError(f'Object {o.name=} not in scene and cant be selected')
o.select_set(True)
-
-def delete(objs):
+def delete(objs: bpy.types.Object | list[bpy.types.Object]):
if not isinstance(objs, list):
objs = [objs]
select_none()
- select(objs)
- with Suppress():
- bpy.ops.object.delete()
+ for obj in objs:
+ select(obj)
+ is_mesh = obj.type == 'MESH'
+ if is_mesh:
+ mesh = obj.data
+ with Suppress():
+ bpy.ops.object.delete()
+ if is_mesh and mesh.users == 0:
+ bpy.data.meshes.remove(mesh)
-def delete_collection(collection):
+def delete_collection(collection: bpy.types.Collection):
if collection.name in bpy.data.collections:
objects = collection.objects
bpy.data.collections.remove(collection)
@@ -270,10 +326,18 @@ def unlink(obj):
c.objects.unlink(o)
-def put_in_collection(obj, collection, exclusive=True):
- if exclusive:
- unlink(obj)
- collection.objects.link(obj)
+def put_in_collection(objs, collection, exclusive=True):
+ if isinstance(collection, str):
+ collection = get_collection(collection)
+ if isinstance(objs, bpy.types.Object):
+ objs = [objs]
+ else:
+ objs = list(objs)
+ for o in objs:
+ if exclusive:
+ unlink(o)
+ collection.objects.link(o)
+ return collection
def group_in_collection(objs, name: str, reuse=True, **kwargs):
@@ -350,18 +414,63 @@ def spawn_plane(**kwargs):
obj.name = name
return obj
-def spawn_cube(**kwargs):
- name = kwargs.pop('name', None)
+def spawn_cube(size=1, location=(0, 0, 0), scale=(1, 1, 1), name=None):
+
bpy.ops.mesh.primitive_cube_add(
+ size = size,
enter_editmode=False,
align='WORLD',
- **kwargs
+ location=location,
+ scale=scale,
)
obj = bpy.context.active_object
if name is not None:
obj.name = name
return obj
+def spawn_cylinder(radius=1.0, depth=2.0, location=(0, 0, 0), scale=(1, 1, 1), name=None):
+
+ bpy.ops.mesh.primitive_cylinder_add(
+ radius=radius,
+ depth=depth,
+ enter_editmode=False,
+ align='WORLD',
+ location=location,
+ scale=scale,
+ )
+ obj = bpy.context.active_object
+ if name is not None:
+ obj.name = name
+ return obj
+
+def spawn_sphere(radius=1, location=(0, 0, 0), scale=(1, 1, 1), name=None):
+
+ bpy.ops.mesh.primitive_uv_sphere_add(
+ radius = radius,
+ enter_editmode=False,
+ align='WORLD',
+ location=location,
+ scale=scale,
+ )
+ obj = bpy.context.active_object
+ if name is not None:
+ obj.name = name
+ return obj
+
+def spawn_icosphere(radius=1, location=(0, 0, 0), scale=(1, 1, 1), name=None):
+
+ bpy.ops.mesh.primitive_ico_sphere_add(
+ radius = radius,
+ enter_editmode=False,
+ align='WORLD',
+ location=location,
+ scale=scale,
+ )
+ obj = bpy.context.active_object
+ if name is not None:
+ obj.name = name
+ return obj
+
def clear_scene(keep=[], targets=None, materials=True):
D = bpy.data
if targets is None:
@@ -431,7 +540,19 @@ def get_camera_res():
def set_geomod_inputs(mod, inputs: dict):
assert mod.type == 'NODES'
for k, v in inputs.items():
+
+ if k not in mod.node_group.inputs:
+ raise KeyError(f'Couldnt find {k=} in {mod.node_group.inputs.keys()=}')
+
soc = mod.node_group.inputs[k]
+
+ if not hasattr(soc, 'default_value'):
+ if v is not None:
+ raise ValueError(f'Got non-None value {v=} for {soc.identifier=} which has no default value')
+ continue
+ elif v is None:
+ continue
+
if isinstance(soc.default_value, (float, int)):
v = type(soc.default_value)(v)
@@ -491,7 +612,9 @@ def import_mesh(path, **kwargs):
'obj': bpy.ops.import_scene.obj,
'fbx': bpy.ops.import_scene.fbx,
'stl': bpy.ops.import_mesh.stl,
- 'ply': bpy.ops.import_mesh.ply}
+ 'ply': bpy.ops.import_mesh.ply,
+ 'usdc': bpy.ops.wm.usd_import,
+ }
if ext not in funcs:
raise ValueError(
@@ -578,10 +701,11 @@ def apply_modifiers(obj, mod=None, quiet=True):
con = Suppress() if quiet else nullcontext()
with SelectObjects(obj), con:
for m in mod:
+ mod_type = m.type
try:
bpy.ops.object.modifier_apply(modifier=m.name)
except RuntimeError as e:
- if m.type == 'NODES':
+ if mod_type == 'NODES':
logging.warning(f'apply_modifers on {obj.name=} {m.name=} raised {e}, ignoring and returning empty mesh for pre-3.5 compatibility reasons')
bpy.ops.object.modifier_remove(modifier=m.name)
clear_mesh(obj)
@@ -591,6 +715,9 @@ def apply_modifiers(obj, mod=None, quiet=True):
# geometry nodes occasionally introduces empty material slots in 3.6, we consider this an error and remove them
purge_empty_materials(obj)
+ # geometry nodes occasionally introduces empty material slots in 3.6, we consider this an error and remove them
+ purge_empty_materials(obj)
+
def recalc_normals(obj, inside=False):
with ViewportMode(obj, mode='EDIT'):
@@ -719,7 +846,7 @@ def blender_internal_attr(a):
a = a.name
if a.startswith('.'):
return True
- if a in ['material_index', 'uv_map', 'UVMap']:
+ if a in ['material_index', 'uv_map', 'UVMap', 'sharp_face']:
return True
return False
@@ -731,7 +858,7 @@ def merge_by_distance(obj, face_size):
def origin_set(objs, mode, **kwargs):
with SelectObjects(objs):
bpy.ops.object.origin_set(type=mode, **kwargs)
-
+
def apply_geo(obj):
with SelectObjects(obj):
for m in obj.modifiers:
@@ -744,6 +871,10 @@ def avg_approx_vol(objects):
return np.mean([prod(list(o.dimensions)) for o in objects])
def parent_to(a, b, type='OBJECT', keep_transform=False, no_inverse=False, no_transform=False):
+
+ if a.name == b.name:
+ raise ValueError(f'parent_to expects two distinct objects, got {a=} {b=}')
+
select_none()
with SelectObjects([a, b], active=1):
if no_inverse:
@@ -755,7 +886,8 @@ def parent_to(a, b, type='OBJECT', keep_transform=False, no_inverse=False, no_tr
a.location = (0,0,0)
a.rotation_euler = (0,0,0)
- assert a.parent is b
+ if a.parent is not b:
+ raise ValueError(f'parent_to({a=}, {b=}) failed, after execution we saw {a.parent=}')
def apply_matrix_world(obj, verts: np.array):
return mutil.dehomogenize(mutil.homogenize(verts) @ np.array(obj.matrix_world).T)
@@ -785,7 +917,7 @@ def approve_all_drivers():
def count_objects():
count = 0
for obj in bpy.context.scene.objects:
- if obj.type != "MESH":
+ if obj.type != "MESH":
continue
count +=1
return count
@@ -800,11 +932,11 @@ def count_objects():
def count_instance():
depsgraph = bpy.context.evaluated_depsgraph_get()
return len([inst for inst in depsgraph.object_instances if inst.is_instance])
-
-
+
+
def bounds(obj):
- bbox = np.array(obj.bound_box)
- return bbox.min(axis=0), bbox.max(axis=0)
+ points = np.array(obj.bound_box)
+ return points.min(axis=0), points.max(axis=0)
def create_noise_plane(size=50, cuts=10, std=3, levels=3):
bpy.ops.mesh.primitive_grid_add(size=size, x_subdivisions=cuts, y_subdivisions=cuts)
@@ -821,4 +953,4 @@ def purge_empty_materials(obj):
if m.material is not None:
continue
bpy.context.object.active_material_index = i
- bpy.ops.object.material_slot_remove()
\ No newline at end of file
+ bpy.ops.object.material_slot_remove()
diff --git a/infinigen/core/util/camera.py b/infinigen/core/util/camera.py
index 0c0dbff68..fc955eb51 100644
--- a/infinigen/core/util/camera.py
+++ b/infinigen/core/util/camera.py
@@ -1,7 +1,7 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Lahav Lipson
+# Authors: Lahav Lipson, Lingjie Mei
import numpy as np
@@ -21,7 +21,7 @@
# Build intrinsic camera parameters from Blender camera data
#
-# See notes on this in
+# See notes on this in
# blender.stackexchange.com/questions/15102/what-is-blenders-camera-projection-matrix-model
def get_calibration_matrix_K_from_blender(camd):
f_in_mm = camd.lens
@@ -35,20 +35,20 @@ def get_calibration_matrix_K_from_blender(camd):
if sensor_width_in_mm/sensor_height_in_mm != W/H:
vals = f'{(sensor_width_in_mm, sensor_height_in_mm, W, H)=}'
raise ValueError(f'Camera sensor has not been properly configured, you probably need to call camera.adjust_camera_sensor on it. {vals}')
-
+
pixel_aspect_ratio = scene.render.pixel_aspect_x / scene.render.pixel_aspect_y
if (camd.sensor_fit == 'VERTICAL'):
- # the sensor height is fixed (sensor fit is horizontal),
+ # the sensor height is fixed (sensor fit is horizontal),
# the sensor width is effectively changed with the pixel aspect ratio
s_u = resolution_x_in_px * scale / sensor_width_in_mm / pixel_aspect_ratio # pixels per milimeter
s_v = resolution_y_in_px * scale / sensor_height_in_mm
else: # 'HORIZONTAL' and 'AUTO'
- # the sensor width is fixed (sensor fit is horizontal),
+ # the sensor width is fixed (sensor fit is horizontal),
# the sensor height is effectively changed with the pixel aspect ratio
pixel_aspect_ratio = scene.render.pixel_aspect_x / scene.render.pixel_aspect_y
s_u = resolution_x_in_px * scale / sensor_width_in_mm
s_v = resolution_y_in_px * scale * pixel_aspect_ratio / sensor_height_in_mm
-
+
# Parameters of intrinsic calibration matrix K
alpha_u = f_in_mm * s_u
@@ -64,7 +64,7 @@ def get_calibration_matrix_K_from_blender(camd):
return K
# Returns camera rotation and translation matrices from Blender.
-#
+#
# There are 3 coordinate systems involved:
# 1. The World coordinates: "world"
# - right-handed
@@ -74,7 +74,7 @@ def get_calibration_matrix_K_from_blender(camd):
# - right-handed: negative z look-at direction
# 3. The desired computer vision camera coordinates: "cv"
# - x is horizontal
-# - y is down (to align to the actual pixel coordinates
+# - y is down (to align to the actual pixel coordinates
# used in digital images)
# - right-handed: positive z look-at direction
def get_3x4_RT_matrix_from_blender(cam):
@@ -84,7 +84,7 @@ def get_3x4_RT_matrix_from_blender(cam):
(0, -1, 0),
(0, 0, -1)))
- # Transpose since the rotation is object rotation,
+ # Transpose since the rotation is object rotation,
# and we want coordinate rotation
# R_world2bcam = cam.rotation_euler.to_matrix().transposed()
# T_world2bcam = -1*R_world2bcam * location
@@ -95,7 +95,7 @@ def get_3x4_RT_matrix_from_blender(cam):
# Convert camera location to translation vector used in coordinate changes
# T_world2bcam = -1*R_world2bcam*cam.location
- # Use location from matrix_world to account for constraints:
+ # Use location from matrix_world to account for constraints:
T_world2bcam = -1*R_world2bcam @ location
# Build the coordinate transform matrix from world to computer vision camera
@@ -139,16 +139,16 @@ def compute_vis_dists(points, cam):
clamped_uv = np.clip(uv, [0, 0], butil.get_camera_res())
clamped_d = np.maximum(d, 0)
-
+
RT_4x4_inv = np.array(Matrix(RT).to_4x4().inverted())
clipped_pos = homogenize((homogenize(clamped_uv) * clamped_d[:, None]) @ np.linalg.inv(K).T) @ RT_4x4_inv.T
-
+
vis_dist = np.linalg.norm(points[:, :-1] - clipped_pos[:, :-1], axis=-1)
return d, vis_dist
def min_dists_from_cam_trajectory(points, cam, start=None, end=None, verbose=False):
-
+
assert len(points.shape) == 2 and points.shape[-1] == 3
assert cam.type == 'CAMERA'
@@ -167,9 +167,13 @@ def min_dists_from_cam_trajectory(points, cam, start=None, end=None, verbose=Fal
dists, vis_dists = compute_vis_dists(points, cam)
min_dists = np.minimum(dists, min_dists)
min_vis_dists = np.minimum(vis_dists, min_vis_dists)
-
- return min_dists, min_vis_dists
-
+ return min_dists, min_vis_dists
+def points_inview(bbox, camera):
+ proj = np.array(get_3x4_P_matrix_from_blender(camera)[0])
+ x, y, z = proj @ np.concatenate([bbox, np.ones((len(bbox), 1))], -1).T
+ render = bpy.context.scene.render
+ inview = (z > 0) & (x >= 0) & (y >= 0) & (x / z < render.resolution_x) & (y / z < render.resolution_y)
+ return inview
diff --git a/infinigen/core/util/color.py b/infinigen/core/util/color.py
index 90979f273..f91e3c205 100644
--- a/infinigen/core/util/color.py
+++ b/infinigen/core/util/color.py
@@ -1,5 +1,6 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
# Authors: Alexander Raistrick, Yiming Zuo, Lingjie Mei, Lahav Lipson
@@ -15,6 +16,7 @@
from infinigen.core.util.math import int_hash
+
@dataclass
class ChannelScheme:
args: list
@@ -30,105 +32,41 @@ def sample(self):
v = np.clip(v, *self.clip)
return v
+
U = lambda min, max, **kwargs: ChannelScheme([min, max], dist='uniform', **kwargs)
N = lambda m, std, **kwargs: ChannelScheme([m, std], dist='normal', **kwargs)
HSV_RANGES = {
- 'petal': (
- N(0.95, 1.2, wrap=True),
- U(0.2, 0.85),
- U(0.2, 0.75)),
- 'gem': (
- U(0, 1),
- U(0.85, 0.85),
- U(0.5, 1)),
- 'greenery': (
- U(0.25, 0.33),
- N(0.65, 0.03),
- U(0.1, 0.45)
- ),
- 'yellowish': (
- N(0.15, 0.005, wrap=True),
- N(0.95, 0.02),
- N(0.9, 0.02)
- ),
- 'red': (
- N(0.0, 0.05, wrap=True),
- N(0.9, 0.03),
- N(0.6, 0.05)
- ),
- 'pink': (
- N(0.88, 0.06, wrap=True),
- N(0.6, 0.05),
- N(0.8, 0.05)
- ),
- 'white': (
- N(0.0, 0.06, wrap=True),
- U(0.0, 0.2, clip=[0, 1]),
- N(0.95, 0.02)
- ),
- 'fog': (
- U(0, 1),
- U(0, 0.2),
- U(0.8, 1)
- ),
- 'water': (
- U(0.2, 0.6),
- N(0.5, 0.1),
- U(0.7, 1)
- ),
- 'darker_water': (
- U(0.2, 0.6),
- N(0.5, 0.1),
- U(0.2, 0.3)
- ),
- 'under_water': (
- U(0.5, 0.7),
- U(0.7, 0.95),
- U(0.7, 1)
- ),
- 'eye_schlera': (
- U(0.05, 0.15),
- U(0.2, 0.8),
- U(0.05, 0.5)
- ),
- 'eye_pupil': (
- U(0, 1),
- U(0.1, 0.9),
- U(0.1, 0.9)
- ),
- 'beak': (
- U(0, 0.13),
- U(0, 0.9),
- U(0.1, 0.6)
- ),
- 'fur': (
- U(0, 0.11),
- U(0.5, 0.95),
- U(0.02, 0.9)
- ),
- 'pine_needle': (
- N(0.05, 0.02, wrap=True),
- U(0.5, 0.93),
- U(0.045, 0.4),
- ),
- 'wet_sand': (
- U(0.05, 0.1),
- U(0.65, 0.7),
- U(0.05, 0.15),
- ),
- 'dry_sand': (
- U(0.05, 0.1),
- U(0.65, 0.7),
- U(0.15, 0.25),
- ),
- #'dirt': ('uniform', [], []),
- #'rock': ('uniform', [], []),
- #'creature_fur': ('normal', [0.89, 0.6, 0.2], []),
- #'creature_scale': ('uniform', [], []),
- #'wood': ('uniform', [], []),
+ 'petal': (N(0.95, 1.2, wrap=True), U(0.2, 0.85), U(0.2, 0.75)),
+ 'gem': (U(0, 1), U(0.85, 0.85), U(0.5, 1)),
+ 'greenery': (U(0.25, 0.33), N(0.65, 0.03), U(0.1, 0.45)),
+ 'yellowish': (N(0.15, 0.005, wrap=True), N(0.95, 0.02), N(0.9, 0.02)),
+ 'red': (N(0.0, 0.05, wrap=True), N(0.9, 0.03), N(0.6, 0.05)),
+ 'pink': (N(0.88, 0.06, wrap=True), N(0.6, 0.05), N(0.8, 0.05)),
+ 'white': (N(0.0, 0.06, wrap=True), U(0.0, 0.2, clip=[0, 1]), N(0.95, 0.02)),
+ 'fog': (U(0, 1), U(0, 0.2), U(0.8, 1)),
+ 'water': (U(0.2, 0.6), N(0.5, 0.1), U(0.7, 1)),
+ 'darker_water': (U(0.2, 0.6), N(0.5, 0.1), U(0.2, 0.3)),
+ 'under_water': (U(0.5, 0.7), U(0.7, 0.95), U(0.7, 1)),
+ 'eye_schlera': (U(0.05, 0.15), U(0.2, 0.8), U(0.05, 0.5)),
+ 'eye_pupil': (U(0, 1), U(0.1, 0.9), U(0.1, 0.9)),
+ 'beak': (U(0, 0.13), U(0, 0.9), U(0.1, 0.6)),
+ 'fur': (U(0, 0.11), U(0.5, 0.95), U(0.02, 0.9)),
+ 'pine_needle': (N(0.05, 0.02, wrap=True), U(0.5, 0.93), U(0.045, 0.4),),
+ 'wet_sand': (U(0.05, 0.1), U(0.65, 0.7), U(0.05, 0.15),),
+ 'dry_sand': (U(0.05, 0.1), U(0.65, 0.7), U(0.15, 0.25),),
+ 'leather': (U(0.04, 0.07), U(0.80, 1.0), U(0.1, 0.6),),
+ 'concrete': (U(0.0, 1.0), U(0.02, 0.12), U(0.3, 0.9),),
+ 'textile': (U(0, 1), U(0.15, 0.7), U(0.1, 0.3),),
+ 'fabric': (U(0, 1), U(0.3, 0.8), U(0.6, 0.9))
+ # 'dirt': ('uniform', [], []),
+ # 'rock': ('uniform', [], []),
+ # 'creature_fur': ('normal', [0.89, 0.6, 0.2], []),
+ # 'creature_scale': ('uniform', [], []),
+ # 'wood': ('uniform', [], []),
}
+
def color_category(name):
if not name in HSV_RANGES:
raise ValueError(f'color_category did not recognize {name=}, options are {HSV_RANGES.keys()=}')
@@ -137,17 +75,43 @@ def color_category(name):
hsv = [s.sample() for s in schemes]
return hsv2rgba(hsv)
-def hsv2rgba(hsv):
+
+def hsv2rgba(hsv, *args):
# hsv is a len-3 tuple or array
c = mathutils.Color()
- c.hsv = list(hsv)
+ if len(args) > 0:
+ hsv = hsv, *args
+ c.hsv = np.array([hsv[0] % 1, hsv[1], hsv[2]])
rgba = list(c) + [1]
return np.array(rgba)
+
+def rgb2hsv(rgb, *args):
+ # hsv is a len-3 tuple or array
+ c = mathutils.Color()
+ if len(args) > 0:
+ rgb = rgb, *args
+ c.r, c.g, c.b = rgb
+ return np.array(c.hsv)
+
+
+def srgb_to_linearrgb(c):
+ if c < 0: return 0
+ elif c < 0.04045: return c / 12.92
+ else: return ((c + 0.055) / 1.055) ** 2.4
+
+
+def hex2rgba(h, alpha=1):
+ r = (h & 0xff0000) >> 16
+ g = (h & 0x00ff00) >> 8
+ b = (h & 0x0000ff)
+ return tuple([srgb_to_linearrgb(c / 0xff) for c in (r, g, b)] + [alpha])
+
+
@gin.configurable
def random_color_mapping(color_tuple, scene_seed, hue_stddev):
- r,g,b,a = color_tuple
+ r, g, b, a = color_tuple
h, s, v = colorsys.rgb_to_hsv(r, g, b)
- color_hash = int_hash((int(h*1e3), scene_seed))
+ color_hash = int_hash((int(h * 1e3), scene_seed))
h = np.random.RandomState(color_hash).normal(h, hue_stddev) % 1.0
return colorsys.hsv_to_rgb(h, s, v) + (a,)
diff --git a/infinigen/core/util/exporting.py b/infinigen/core/util/exporting.py
index 24e9b62af..eb50c61a1 100644
--- a/infinigen/core/util/exporting.py
+++ b/infinigen/core/util/exporting.py
@@ -29,7 +29,7 @@ def get_mesh_data(obj):
verts.foreach_get("co", vert_lookup)
vert_lookup = vert_lookup.reshape((-1, 3))
masktag = np.full(len(verts, ), 0, dtype=np.int32)
- if 'MaskTag' in obj.data.attributes:
+ if False and 'MaskTag' in obj.data.attributes:
obj.data.attributes['MaskTag'].data.foreach_get("value", masktag)
assert (loop_totals.size == 0) or (loop_totals.min() >= 0)
assert (indices.size == 0) or (indices.min() >= 0)
diff --git a/infinigen/core/util/logging.py b/infinigen/core/util/logging.py
index a187ce741..cb2389f90 100644
--- a/infinigen/core/util/logging.py
+++ b/infinigen/core/util/logging.py
@@ -1,10 +1,11 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors:
+# Authors:
# - Lahav Lipson: logging formats, timer format
# - Alex Raistrick: Timer
# - Alejandro Newell: Suppress
+# - Lingjie Mei: disable
import os, sys
@@ -23,7 +24,7 @@ class Timer:
def __init__(self, desc, disable_timer=False, logger=None):
self.disable_timer = disable_timer
- if self.disable_timer:
+ if self.disable_timer:
return
self.name = f'[{desc}]'
if logger is None:
@@ -54,11 +55,14 @@ def __enter__(self, logfile=os.devnull):
sys.stdout.flush()
os.close(1)
os.open(logfile, os.O_WRONLY)
+ self.level = logging.root.manager.disable
+ logging.disable(logging.CRITICAL)
def __exit__(self, type, value, traceback):
os.close(1)
os.dup(self.old)
os.close(self.old)
+ logging.disable(self.level)
class LogLevel():
@@ -88,3 +92,7 @@ def create_text_file(log_dir, filename, text=None):
(log_dir / filename).touch()
if text is not None:
(log_dir / filename).write_text(text)
+
+
+class BadSeedError(ValueError):
+ pass
\ No newline at end of file
diff --git a/infinigen/core/util/organization.py b/infinigen/core/util/organization.py
index ad57e1b38..a2e264688 100644
--- a/infinigen/core/util/organization.py
+++ b/infinigen/core/util/organization.py
@@ -11,6 +11,7 @@ class Task:
Render = "render"
GroundTruth = "ground_truth"
MeshSave = "mesh_save"
+ Export = "export"
class Materials:
diff --git a/infinigen/core/util/pipeline.py b/infinigen/core/util/pipeline.py
index 09826fb76..3e19a5445 100644
--- a/infinigen/core/util/pipeline.py
+++ b/infinigen/core/util/pipeline.py
@@ -39,12 +39,12 @@ def _should_run_stage(self, name, use_chance, prereq):
logger.info(f'Skipping run_stage({name}...) due to unmet {prereq=}')
return
with FixedSeed(int_hash((self.scene_seed, name, 0))):
+ if not self.params.get(f'{name}_enabled', True):
+ logger.debug(f'Not running {name} due to manually set not enabled')
+ return False
if use_chance and np.random.uniform() > self.params[f'{name}_chance']:
logger.debug(f'Not running {name} due to random chance')
return False
- if not use_chance and not self.params.get(f'{name}_enabled', True):
- logger.debug(f'Not running {name} due to manually set not enabled')
- return False
return True
def save_results(self, path):
diff --git a/infinigen/core/util/random.py b/infinigen/core/util/random.py
index 1f34bd5f5..156466e96 100644
--- a/infinigen/core/util/random.py
+++ b/infinigen/core/util/random.py
@@ -18,12 +18,12 @@
from infinigen.core.init import repo_root
-def log_uniform(low, high, size=1):
+def log_uniform(low, high, size=None):
return np.exp(uniform(np.log(low), np.log(high), size))
def sample_json_palette(pallette_name, n_sample=1):
- rel = f"infinigen_examples/configs/palette/{pallette_name}.json"
+ rel = f"infinigen_examples/configs_nature/palette/{pallette_name}.json"
with (repo_root()/rel).open('r') as f:
color_template = json.load(f)
@@ -52,6 +52,7 @@ def sample_json_palette(pallette_name, n_sample=1):
return color
return color_samples
+
def random_general(var):
if not (isinstance(var, tuple) or isinstance(var, list)):
return var
@@ -80,13 +81,16 @@ def random_general(var):
elif func == "power_uniform":
return 10 ** np.random.uniform(*args)
elif func == "log_uniform":
- return log_uniform(*args)[0]
+ return log_uniform(*args)
elif func == "discrete_uniform":
return np.random.randint(args[0], args[1] + 1)
elif func == "bool":
return np.random.uniform() < args[0]
elif func == "choice":
- return np.random.choice(args[0], 1, p=args[1])[0]
+ return np.random.choice(args[0], 1, p=args[1] if len(args) > 1 else None)[0]
+ elif func == 'categorical':
+ prob = np.array(args)
+ return np.random.choice(np.arange(len(args)), p=prob/prob.sum())
elif func == "palette":
return sample_json_palette(*args)
elif func == "color_category":
@@ -202,4 +206,5 @@ def random_color(brightness_lim=1):
def sample_registry(reg):
classes, weights = zip(*reg)
weights = np.array(weights)
- return np.random.choice(classes, p=weights/weights.sum())
\ No newline at end of file
+ return np.random.choice(classes, p=weights/weights.sum())
+
diff --git a/infinigen/datagen/configs/base.gin b/infinigen/datagen/configs/base.gin
index 8b05f57ea..08eaeb909 100644
--- a/infinigen/datagen/configs/base.gin
+++ b/infinigen/datagen/configs/base.gin
@@ -11,4 +11,4 @@ sample_scene_spec.config_distribution = [
("coast", 4),
("arctic", 1),
("snowy_mountain", 1),
-]
\ No newline at end of file
+]
diff --git a/infinigen/datagen/configs/compute_platform/local_128GB.gin b/infinigen/datagen/configs/compute_platform/local_128GB.gin
index bff783e2b..374dfab6e 100644
--- a/infinigen/datagen/configs/compute_platform/local_128GB.gin
+++ b/infinigen/datagen/configs/compute_platform/local_128GB.gin
@@ -1,4 +1,4 @@
-include 'compute_platform/local_256GB.gin'
+include 'infinigen/datagen/configs/compute_platform/local_256GB.gin'
manage_datagen_jobs.num_concurrent=8
diff --git a/infinigen/datagen/configs/compute_platform/local_16GB.gin b/infinigen/datagen/configs/compute_platform/local_16GB.gin
index 83041fdad..5150853b8 100644
--- a/infinigen/datagen/configs/compute_platform/local_16GB.gin
+++ b/infinigen/datagen/configs/compute_platform/local_16GB.gin
@@ -1,4 +1,4 @@
-include 'compute_platform/local_256GB.gin'
+include 'infinigen/datagen/configs/compute_platform/local_256GB.gin'
manage_datagen_jobs.num_concurrent=1
diff --git a/infinigen/datagen/configs/compute_platform/local_256GB.gin b/infinigen/datagen/configs/compute_platform/local_256GB.gin
index 211feaecd..babc757f8 100644
--- a/infinigen/datagen/configs/compute_platform/local_256GB.gin
+++ b/infinigen/datagen/configs/compute_platform/local_256GB.gin
@@ -22,6 +22,11 @@ queue_combined.cpus = 2
queue_combined.hours = 48
queue_combined.submit_cmd = @local_submit_cmd
+# Export
+queue_export.cpus = 4
+queue_export.hours = 24
+queue_export.submit_cmd = @local_submit_cmd
+
# Rendering
queue_render.cpus = 4
queue_render.submit_cmd = @local_submit_cmd
diff --git a/infinigen/datagen/configs/compute_platform/local_64GB.gin b/infinigen/datagen/configs/compute_platform/local_64GB.gin
index 51c6baa03..ca98c6f5d 100644
--- a/infinigen/datagen/configs/compute_platform/local_64GB.gin
+++ b/infinigen/datagen/configs/compute_platform/local_64GB.gin
@@ -1,4 +1,4 @@
-include 'compute_platform/local_256GB.gin'
+include 'infinigen/datagen/configs/compute_platform/local_256GB.gin'
manage_datagen_jobs.num_concurrent=3
diff --git a/infinigen/datagen/configs/compute_platform/slurm.gin b/infinigen/datagen/configs/compute_platform/slurm.gin
index c12d23ff1..fb9d2434d 100644
--- a/infinigen/datagen/configs/compute_platform/slurm.gin
+++ b/infinigen/datagen/configs/compute_platform/slurm.gin
@@ -1,10 +1,10 @@
-manage_datagen_jobs.num_concurrent = 'ENVVAR_INFINIGEN_NUMCONCURRENT_TARGET'
+manage_datagen_jobs.num_concurrent = 100
slurm_submit_cmd.slurm_account = 'ENVVAR_INFINIGEN_SLURMPARTITION' # change to partitionname string, or None
slurm_submit_cmd.slurm_niceness = 10000
get_slurm_banned_nodes.config_path = 'ENVVAR_INFINIGEN_SLURM_EXCLUDENODES_LIST'
-jobs_to_launch_next.max_queued_total = 10
-jobs_to_launch_next.max_stuck_at_task = 8
+jobs_to_launch_next.max_queued_total = 40
+jobs_to_launch_next.max_stuck_at_task = 40
# Combined (only used when `stereo_combined.gin` or similar is included)
queue_combined.mem_gb = 12
@@ -17,7 +17,7 @@ renderbackup/queue_combined.mem_gb = 24
queue_coarse.mem_gb = 24
queue_coarse.cpus = 4
queue_coarse.hours = 48
-queue_coarse.submit_cmd = @slurm_submit_cmd
+queue_coarse.submit_cmd = @coarse/slurm_submit_cmd
queue_coarse.exclude_gpus = ['a6000', 'rtx_3090']
# Fine terrain
@@ -34,6 +34,13 @@ queue_populate.submit_cmd = @slurm_submit_cmd
renderbackup/queue_populate.mem_gb = 24
queue_populate.exclude_gpus = ['a6000', 'rtx_3090']
+# Export
+queue_export.mem_gb = 50
+queue_export.cpus = 4
+queue_export.hours = 24
+queue_export.submit_cmd = @slurm_submit_cmd
+queue_export.exclude_gpus = ['a6000', 'rtx_3090']
+
# Rendering
queue_render.submit_cmd = @slurm_submit_cmd
queue_render.hours = 48
diff --git a/infinigen/datagen/configs/compute_platform/slurm_1h.gin b/infinigen/datagen/configs/compute_platform/slurm_1h.gin
index ab90e08b3..dc3ed03e1 100644
--- a/infinigen/datagen/configs/compute_platform/slurm_1h.gin
+++ b/infinigen/datagen/configs/compute_platform/slurm_1h.gin
@@ -1,8 +1,9 @@
-include 'compute_platform/slurm.gin'
+include 'infinigen/datagen/configs/compute_platform/slurm.gin'
slurm_submit_cmd.slurm_niceness = 0
-iterate_scene_tasks.view_block_size = 3
+iterate_scene_tasks.cam_block_size = 2
+slurm_submit_cmd.slurm_niceness = 0
queue_combined.hours = 1
queue_coarse.hours = 1
@@ -14,4 +15,4 @@ queue_opengl.hours = 1
queue_coarse.cpus = 8
-queue_upload.hours = 24
\ No newline at end of file
+queue_upload.hours = 24
diff --git a/infinigen/datagen/configs/compute_platform/slurm_cpuheavy.gin b/infinigen/datagen/configs/compute_platform/slurm_cpuheavy.gin
index d692aad2b..a13d74e58 100644
--- a/infinigen/datagen/configs/compute_platform/slurm_cpuheavy.gin
+++ b/infinigen/datagen/configs/compute_platform/slurm_cpuheavy.gin
@@ -1,4 +1,4 @@
-include 'tools/pipeline_configs/compute_platform/slurm.gin'
+include 'infinigen/datagen/compute_platform/slurm.gin'
iterate_scene_tasks.view_block_size = 2
diff --git a/infinigen/datagen/configs/compute_platform/slurm_high_memory.gin b/infinigen/datagen/configs/compute_platform/slurm_high_memory.gin
index b121518db..41f4d6cc2 100644
--- a/infinigen/datagen/configs/compute_platform/slurm_high_memory.gin
+++ b/infinigen/datagen/configs/compute_platform/slurm_high_memory.gin
@@ -1,4 +1,4 @@
-include 'compute_platform/slurm.gin'
+include 'infinigen/datagen/configs/compute_platform/slurm.gin'
# Combined (only used when `stereo_combined.gin` or similar is included)
queue_combined.mem_gb = 48
@@ -10,4 +10,8 @@ queue_fine_terrain.cpus = 4
queue_populate.mem_gb = 48
queue_populate.cpus = 4
queue_populate.hours = 24
+queue_export.mem_gb = 48
+queue_export.cpus = 4
+queue_export.hours = 24
+queue_export.submit_cmd = @local_submit_cmd
renderbackup/queue_populate.mem_gb = 48
diff --git a/infinigen/datagen/configs/data_schema/monocular_flow.gin b/infinigen/datagen/configs/data_schema/monocular_flow.gin
index d55133bd9..0bee65ae8 100644
--- a/infinigen/datagen/configs/data_schema/monocular_flow.gin
+++ b/infinigen/datagen/configs/data_schema/monocular_flow.gin
@@ -1,4 +1,4 @@
-include 'data_schema/monocular.gin'
+include 'infinigen/datagen/data_schema/monocular.gin'
iterate_scene_tasks.frame_range=(1, 2)
diff --git a/infinigen/datagen/configs/data_schema/stereo_video.gin b/infinigen/datagen/configs/data_schema/stereo_video.gin
index d0d755393..0f3082e03 100644
--- a/infinigen/datagen/configs/data_schema/stereo_video.gin
+++ b/infinigen/datagen/configs/data_schema/stereo_video.gin
@@ -1,2 +1,2 @@
-include 'data_schema/monocular_video.gin'
+include 'infinigen/datagen/data_schema/monocular_video.gin'
iterate_scene_tasks.cam_id_ranges = [1, 2]
diff --git a/infinigen/datagen/configs/export.gin b/infinigen/datagen/configs/export.gin
new file mode 100644
index 000000000..8493cf94c
--- /dev/null
+++ b/infinigen/datagen/configs/export.gin
@@ -0,0 +1,3 @@
+iterate_scene_tasks.finalize_tasks = [
+ {'name': "export", 'func': @queue_export}
+]
\ No newline at end of file
diff --git a/infinigen/datagen/configs/gt_options/opengl_gt_noshortrender.gin b/infinigen/datagen/configs/gt_options/opengl_gt_noshortrender.gin
index 8ce7aee7c..fbaf1a66b 100644
--- a/infinigen/datagen/configs/gt_options/opengl_gt_noshortrender.gin
+++ b/infinigen/datagen/configs/gt_options/opengl_gt_noshortrender.gin
@@ -1,4 +1,4 @@
-include 'gt_options/opengl_gt.gin' # incase someone adds other settings to it
+include 'infinigen/datagen/gt_options/opengl_gt.gin' # incase someone adds other settings to it
iterate_scene_tasks.camera_dependent_tasks = [
{'name': 'renderbackup', 'func': @renderbackup/queue_render}, # still call it "backup" since it is reusing the compute_platform's backup config. we are just skipping straight to the backup
diff --git a/infinigen/datagen/configs/indoor_background_configs.gin b/infinigen/datagen/configs/indoor_background_configs.gin
new file mode 100644
index 000000000..b75e6b32d
--- /dev/null
+++ b/infinigen/datagen/configs/indoor_background_configs.gin
@@ -0,0 +1,10 @@
+sample_scene_spec.config_distribution = [
+ ("forest", 4),
+ ("river", 4),
+ ("desert", 4),
+ ("mountain", 2),
+ ("canyon", 2),
+ ("plain", 4),
+ ("coast", 8),
+ #("snowy_mountain", 4), # disabled until fast_terrain_assets.gin works for snow sim @mazeyu
+]
\ No newline at end of file
diff --git a/infinigen/datagen/configs/opengl_gt_noshortrender.gin b/infinigen/datagen/configs/opengl_gt_noshortrender.gin
deleted file mode 100644
index 5b07d93e1..000000000
--- a/infinigen/datagen/configs/opengl_gt_noshortrender.gin
+++ /dev/null
@@ -1,7 +0,0 @@
-include 'gt_options/opengl_gt.gin' # incase someone adds other settings to it
-
-iterate_scene_tasks.camera_dependent_tasks = [
- {'name': 'renderbackup', 'func': @renderbackup/queue_render}, # still call it "backup" since it is reusing the compute_platform's backup config. we are just skipping straight to the backup
- {'name': 'savemesh', 'func': @queue_mesh_save},
- {'name': 'opengl', 'func': @queue_opengl}
-]
diff --git a/infinigen/datagen/configs/upload.gin b/infinigen/datagen/configs/upload.gin
index d0d71d163..4c8e7b78e 100644
--- a/infinigen/datagen/configs/upload.gin
+++ b/infinigen/datagen/configs/upload.gin
@@ -1,4 +1,5 @@
iterate_scene_tasks.finalize_tasks = [
+ {'name': "export", 'func': @queue_export},
{'name': "upload", 'func': @queue_upload}
]
diff --git a/infinigen/datagen/job_funcs.py b/infinigen/datagen/job_funcs.py
index 7394ae655..a10906fcd 100644
--- a/infinigen/datagen/job_funcs.py
+++ b/infinigen/datagen/job_funcs.py
@@ -4,8 +4,10 @@
# Authors:
# - Alex Raistrick: refactor, local rendering, video rendering
# - Lahav Lipson: stereo version, local rendering
+# - David Yan: export integration
# - Hei Law: initial version
+
import re
import gin
from copy import copy
@@ -75,6 +77,38 @@ def queue_upload(folder, submit_cmd, name, taskname, dir_prefix_len=0, method='r
res = submit_cmd((func, folder, taskname), folder, name, **kwargs)
return res, None
+
+@gin.configurable
+def queue_export(
+ folder,
+ submit_cmd,
+ name,
+ seed,
+ configs,
+ taskname=None,
+ exclude_gpus=[],
+ overrides=[],
+ input_indices=None, output_indices=None,
+ **kwargs
+):
+ input_suffix = get_suffix(input_indices)
+ input_folder=f'{folder}/coarse{input_suffix}'
+
+ cmd = get_cmd(seed, 'export', configs, taskname, output_folder=f'{folder}/frames', input_folder=input_folder)+ f'''
+ LOG_DIR='{folder / "logs"}'
+ '''.split("\n") + overrides
+
+ with (folder / "run_pipeline.sh").open('a') as f:
+ f.write(f"{' '.join(' '.join(cmd).split())}\n\n")
+
+ res = submit_cmd(cmd,
+ folder=folder,
+ name=name,
+ gpus=0,
+ **kwargs
+ )
+ return res, folder
+
@gin.configurable
def queue_coarse(
folder,
@@ -266,8 +300,22 @@ def queue_render(
output_folder = Path(f'{folder}/frames{output_suffix}')
+ input_folder_priority_options = [
+ f"fine{input_suffix}",
+ "fine",
+ f"coarse{input_suffix}",
+ "coarse"
+ ]
+
+ for option in input_folder_priority_options:
+ input_folder = f'{folder}/{option}'
+ if (Path(input_folder)/'scene.blend').exists():
+ break
+ else:
+ raise ValueError(f'No scene.blend found in {input_folder} for any of {input_folder_priority_options}')
+
cmd = get_cmd(seed, "render", configs, taskname,
- input_folder=f'{folder}/fine{input_suffix}',
+ input_folder=input_folder,
output_folder=f'{output_folder}') + f'''
render.render_image_func=@{render_type}/render_image
LOG_DIR='{folder / "logs"}'
@@ -310,7 +358,7 @@ def queue_mesh_save(
output_folder.mkdir(parents=True, exist_ok=True)
cmd = get_cmd(seed, "mesh_save", configs, taskname,
- input_folder=f'{folder}/fine{input_suffix}',
+ input_folder=f'{folder}/coarse{input_suffix}',
output_folder=f'{folder}/savemesh{output_suffix}') + f'''
LOG_DIR='{folder / "logs"}'
'''.split("\n") + overrides
diff --git a/infinigen/datagen/manage_jobs.py b/infinigen/datagen/manage_jobs.py
index a20cd78f3..15f68123a 100644
--- a/infinigen/datagen/manage_jobs.py
+++ b/infinigen/datagen/manage_jobs.py
@@ -34,6 +34,7 @@
import pandas as pd
import numpy as np
import submitit
+import submitit.core.utils
from jinja2 import Environment, FileSystemLoader, select_autoescape
ORIG_SYS_PATH = list(sys.path) # Make a new instance of sys.path
@@ -400,8 +401,6 @@ def infer_crash_reason(stdout_file, stderr_file: Path):
return "SIGKILL: 9 (out-of-memory, probably)"
elif "SIGCONT" in error_log:
return "SIGCONT (timeout?)"
- elif "srun: error" in error_log:
- return "srun error"
if not stdout_file.exists():
return f'{stdout_file} not found'
@@ -409,13 +408,29 @@ def infer_crash_reason(stdout_file, stderr_file: Path):
return f'{stderr_file} not found'
output_text = f"{stdout_file.read_text()}\n{stderr_file.read_text()}\n"
- matches = re.findall("(Error:[^\n]+)\n", output_text)
+ matches = re.findall("([^\.\n]*[Ee]rror):(.*)\n", output_text)
- ignore_errors = [
- 'Error: Not freed memory blocks',
+ ignore_errors = {
+
+ # happens for every failed submitit job, not informative to report in summary
+ "FailedProcessError",
+ "CalledProcessError",
+
+ # happens for every failed slurm job on IONIC
+ "srun: error",
+ "FailedJobError",
+ }
+
+ ignore_messages = [
+ "Not freed memory blocks"
]
- matches = [m for m in matches if not any(w in m for w in ignore_errors)]
+ matches = [
+ f'{m[0]}: {m[1]}' for m in matches if not (
+ m[0] in ignore_errors
+ or any(x in m[1] for x in ignore_messages)
+ )
+ ]
if len(matches):
return ','.join(matches)
diff --git a/infinigen/datagen/util/cleanup.py b/infinigen/datagen/util/cleanup.py
index 6923974dc..eef97c5ba 100644
--- a/infinigen/datagen/util/cleanup.py
+++ b/infinigen/datagen/util/cleanup.py
@@ -5,7 +5,6 @@
# - Lahav Lipson
# - Karhan Kayan (cleanup fluid files)
-
import argparse
import shutil
from pathlib import Path
diff --git a/infinigen/datagen/util/upload_util.py b/infinigen/datagen/util/upload_util.py
index 7d52b55d6..7679fec1a 100644
--- a/infinigen/datagen/util/upload_util.py
+++ b/infinigen/datagen/util/upload_util.py
@@ -1,7 +1,9 @@
# Copyright (c) Princeton University.
# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
-# Authors: Lahav Lipson
+# Authors:
+# - Alexander Raistrick: create_upload_payload, metadata
+# - Lahav Lipson: initial version
import argparse
import os
diff --git a/infinigen/launch_blender.py b/infinigen/launch_blender.py
index 5c294d938..11db8205d 100644
--- a/infinigen/launch_blender.py
+++ b/infinigen/launch_blender.py
@@ -1,3 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
import subprocess
import argparse
from pathlib import Path
diff --git a/infinigen/terrain/__init__.py b/infinigen/terrain/__init__.py
index d1420bada..bba71c859 100644
--- a/infinigen/terrain/__init__.py
+++ b/infinigen/terrain/__init__.py
@@ -4,4 +4,4 @@
# Authors: Zeyu Ma
-from .core import Terrain
\ No newline at end of file
+from .core import Terrain, hidden_in_viewport
\ No newline at end of file
diff --git a/infinigen/terrain/core.py b/infinigen/terrain/core.py
index eee150ad1..91eb71e14 100644
--- a/infinigen/terrain/core.py
+++ b/infinigen/terrain/core.py
@@ -23,8 +23,7 @@
from infinigen.core.util.logging import Timer
from infinigen.core.util.math import FixedSeed, int_hash
from infinigen.core.util.organization import SurfaceTypes, Attributes, Task, TerrainNames, ElementNames, Transparency, Materials, Assets, ElementTag, Tags, SelectionCriterions
-from infinigen.assets.utils.tag import tag_object, tag_system
-
+from infinigen.core.tagging import tag_object, tag_system
from numpy import ascontiguousarray as AC
logger = logging.getLogger(__name__)
@@ -87,6 +86,8 @@ def __init__(
main_terrain=TerrainNames.OpaqueTerrain,
under_water=False,
min_distance=1,
+ height_offset=0,
+ whole_bbox=None,
populated_bounds=(-75, 75, -75, 75, -25, 55),
bounds=(-500, 500, -500, 500, -500, 500),
):
@@ -129,6 +130,10 @@ def __init__(
transfer_scene_info(self, scene_infos)
Terrain.instance = self
+ for e in self.elements:
+ self.elements[e].height_offset = height_offset
+ self.elements[e].whole_bbox = whole_bbox
+
def __del__(self):
self.cleanup()
@@ -393,11 +398,11 @@ def build_terrain_bvh_and_attrs(self, terrain_tags_queries, avoid_border=False,
altitude = terrain_mesh.vertices[:, 2]
camera_selection_answers[q0] = terrain_mesh.facewise_mean((altitude > min_altitude) & (altitude < max_altitude))
else:
- camera_selection_answers[q0] = np.zeros(len(terrain_mesh.vertices), dtype=bool)
+ camera_selection_answers[q0] = np.zeros(len(terrain_mesh.faces), dtype=bool)
for key in self.tag_dict:
if set(q).issubset(set(key.split('.'))):
- camera_selection_answers[q0] |= (terrain_mesh.vertex_attributes["MaskTag"] == self.tag_dict[key]).reshape(-1)
- camera_selection_answers[q0] = terrain_mesh.facewise_mean(camera_selection_answers[q0].astype(np.float64))
+ camera_selection_answers[q0] |= (terrain_mesh.face_attributes["MaskTag"] == self.tag_dict[key]).reshape(-1)
+ camera_selection_answers[q0] = camera_selection_answers[q0].astype(np.float64)
if np.abs(np.asarray(terrain_obj.matrix_world) - np.eye(4)).max() > 1e-4:
raise ValueError(f"Not all transformations on {terrain_obj.name} have been applied. This function won't work correctly.")
@@ -421,22 +426,27 @@ def build_terrain_bvh_and_attrs(self, terrain_tags_queries, avoid_border=False,
vertexwise_min_dist = terrain_mesh.facewise_mean(terrain_mesh.vertex_attributes["vertexwise_min_dist"].reshape(-1))
depsgraph = bpy.context.evaluated_depsgraph_get()
- terrain_bvh = BVHTree.FromObject(terrain_obj, depsgraph)
+ scene_bvh = BVHTree.FromObject(terrain_obj, depsgraph)
delete(terrain_obj)
- return terrain_bvh, camera_selection_answers, vertexwise_min_dist
+ return scene_bvh, camera_selection_answers, vertexwise_min_dist
def tag_terrain(self, obj):
if len(obj.data.vertices) == 0: return
+
+
+ mesh = Mesh(obj=obj)
first_time = 1
#initialize with element tag
element_tag = np.zeros(len(obj.data.vertices), dtype=np.int32)
obj.data.attributes[Attributes.ElementTag].data.foreach_get("value", element_tag)
+ element_tag_f = mesh.facewise_intmax(element_tag)
+
for i in range(ElementTag.total_cnt):
- mask_i = element_tag == i
+ mask_i = element_tag_f == i
if mask_i.any():
- obj.data.attributes.new(name=f"TAG_{ElementTag.map[i]}", type="FLOAT", domain='POINT')
+ obj.data.attributes.new(name=f"TAG_{ElementTag.map[i]}", type="FLOAT", domain='FACE')
obj.data.attributes[f"TAG_{ElementTag.map[i]}"].data.foreach_set("value", AC(mask_i.astype(np.float32)))
if first_time:
# "landscape" is a collective name for terrain and water
@@ -445,86 +455,29 @@ def tag_terrain(self, obj):
else:
tag_object(obj)
obj.data.attributes.remove(obj.data.attributes[Attributes.ElementTag])
- # consider cave
- if Tags.Cave in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Tags.Cave].data.foreach_get("value", tag)
- tag = tag > 0.5
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Tags.Cave}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Tags.Cave}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider liquid covered
- if Tags.LiquidCovered in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Tags.LiquidCovered].data.foreach_get("value", tag)
- tag = tag > 0.5
- obj.data.attributes.remove(obj.data.attributes[Tags.LiquidCovered])
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Tags.LiquidCovered}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Tags.LiquidCovered}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider erosion collection
- if Materials.Eroded in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Materials.Eroded].data.foreach_get("value", tag)
- tag = tag > 0.1
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Materials.Eroded}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Materials.Eroded}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider lava
- if Materials.Lava in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Materials.Lava].data.foreach_get("value", tag)
- tag = tag > 0.1
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{ElementNames.Liquid}.{Materials.Lava}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{ElementNames.Liquid}.{Materials.Lava}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider snow
- if Materials.Snow in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Materials.Snow].data.foreach_get("value", tag)
- tag = tag > 0.1
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Materials.Snow}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Materials.Snow}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider lower part of upsidedown mountain
- if Tags.UpsidedownMountainsLowerPart in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Tags.UpsidedownMountainsLowerPart].data.foreach_get("value", tag)
- obj.data.attributes.remove(obj.data.attributes[Tags.UpsidedownMountainsLowerPart])
- tag = tag > 0.5
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Tags.UpsidedownMountainsLowerPart}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Tags.UpsidedownMountainsLowerPart}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- # consider beach
- if Materials.Beach in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Materials.Beach].data.foreach_get("value", tag)
- tag = tag > 0.5
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Materials.Beach}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Materials.Beach}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
- if Tags.OutOfView in obj.data.attributes.keys():
- tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
- obj.data.attributes[Tags.OutOfView].data.foreach_get("value", tag)
- obj.data.attributes.remove(obj.data.attributes[Tags.OutOfView])
- tag = tag > 0.5
- if tag.any():
- obj.data.attributes.new(name=f"TAG_{Tags.OutOfView}", type="FLOAT", domain='POINT')
- obj.data.attributes[f"TAG_{Tags.OutOfView}"].data.foreach_set("value", AC(tag.astype(np.float32)))
- tag_object(obj)
-
+
+ tag_thresholds = [
+ (Tags.Cave, 0.5, 0),
+ (Tags.LiquidCovered, 0.5, 1),
+ (Materials.Eroded, 0.1, 0),
+ (Materials.Lava, 0.1, 0),
+ (Materials.Snow, 0.1, 0),
+ (Tags.UpsidedownMountainsLowerPart, 0.5, 1),
+ (Materials.Beach, 0.5, 0),
+ (Tags.OutOfView, 0.5, 1),
+ ]
+
+ for tag_name, threshold, to_remove in tag_thresholds:
+ if tag_name in obj.data.attributes.keys():
+ tag = np.zeros(len(obj.data.vertices), dtype=np.float32)
+ obj.data.attributes[tag_name].data.foreach_get("value", tag)
+ tag_f = mesh.facewise_mean(tag)
+ tag_f = tag_f > threshold
+ if to_remove:
+ obj.data.attributes.remove(obj.data.attributes[tag_name])
+ if tag_f.any():
+ obj.data.attributes.new(name=f"TAG_{tag_name}", type="FLOAT", domain='FACE')
+ obj.data.attributes[f"TAG_{tag_name}"].data.foreach_set("value", AC(tag_f.astype(np.float32)))
+ tag_object(obj)
+
self.tag_dict = tag_system.tag_dict
diff --git a/infinigen/terrain/elements/core.py b/infinigen/terrain/elements/core.py
index 135f1c95b..46f1870cb 100644
--- a/infinigen/terrain/elements/core.py
+++ b/infinigen/terrain/elements/core.py
@@ -63,9 +63,14 @@ def __init__(self, lib_name, material, transparency):
len(self.int_params3), ASINT(self.int_params3), len(self.float_params3), ASFLOAT(self.float_params3),
)
self.displacement = []
+ self.height_offset = 0
+ self.whole_bbox = None
def __call__(self, positions, sdf_only=False):
+ if self.whole_bbox is not None:
+ mask = (positions >= self.whole_bbox[0].reshape((1, 3))).all(axis=-1) & (positions <= self.whole_bbox[1].reshape((1, 3))).all(axis=-1)
+ positions[:, 2] += self.height_offset
N = len(positions)
sdf = AC(np.zeros(N, dtype=np.float32))
auxs = []
@@ -77,6 +82,10 @@ def __call__(self, positions, sdf_only=False):
else:
auxs.append(None)
self.call(N, ASFLOAT(AC(positions.astype(np.float32))), ASFLOAT(sdf), *[POINTER(c_float)() if x is None else ASFLOAT(x) for x in auxs])
+
+ if self.whole_bbox is not None:
+ sdf[mask] = 1e6
+
ret = {}
ret[Vars.SDF] = sdf
@@ -93,6 +102,7 @@ def __call__(self, positions, sdf_only=False):
for surface in self.displacement:
ret.update(surface({Vars.Position: positions, **ret}))
ret[Vars.SDF] -= ret.pop(Vars.Offset)
+ positions[:, 2] -= self.height_offset
return ret
def get_heightmap(self, X, Y):
diff --git a/infinigen/terrain/utils/camera.py b/infinigen/terrain/utils/camera.py
index b81367d53..59b7bd58c 100644
--- a/infinigen/terrain/utils/camera.py
+++ b/infinigen/terrain/utils/camera.py
@@ -43,7 +43,7 @@ def get_expanded_fov(cam_pose0, cam_poses, fov):
bounds[1] = max(bounds[1], p[0] / p[2])
bounds[2] = min(bounds[2], p[1] / p[2])
bounds[3] = max(bounds[3], p[1] / p[2])
- return (max(-bounds[2], bounds[3]) * 2, max(-bounds[0], bounds[1]) * 2)
+ return (np.arctan(max(-bounds[2], bounds[3])) * 2, np.arctan(max(-bounds[0], bounds[1])) * 2)
@gin.configurable
diff --git a/infinigen/terrain/utils/kernelizer_util.py b/infinigen/terrain/utils/kernelizer_util.py
index 5c68303e5..27b5f998d 100644
--- a/infinigen/terrain/utils/kernelizer_util.py
+++ b/infinigen/terrain/utils/kernelizer_util.py
@@ -68,7 +68,7 @@ class FieldsType:
AttributeType.Int: FieldsType.Value,
AttributeType.FloatVector: FieldsType.Vector,
AttributeType.FloatColor: FieldsType.Color,
- AttributeType.Boolean: FieldsType.Boolean,
+ AttributeType.Boolean: FieldsType.Value,
}
ATTRTYPE_NP = {
diff --git a/infinigen/terrain/utils/mesh.py b/infinigen/terrain/utils/mesh.py
index 9b878b33a..77715d08a 100644
--- a/infinigen/terrain/utils/mesh.py
+++ b/infinigen/terrain/utils/mesh.py
@@ -37,6 +37,18 @@ def object_to_vertex_attributes(obj, specified=None, skip_internal=True):
vertex_attributes[attr] = tmp.reshape((len(obj.data.vertices), -1))
return vertex_attributes
+def object_to_face_attributes(obj, specified=None, skip_internal=True):
+ face_attributes = {}
+ for attr in obj.data.attributes.keys():
+ if skip_internal and butil.blender_internal_attr(attr):
+ continue
+ if ((specified is None) or (specified is not None and attr in specified)) and obj.data.attributes[attr].domain == "FACE":
+ type_key = obj.data.attributes[attr].data_type
+ tmp = np.zeros(len(obj.data.polygons) * ATTRTYPE_DIMS[type_key], dtype=np.float32)
+ obj.data.attributes[attr].data.foreach_get(ATTRTYPE_FIELDS[type_key], tmp)
+ face_attributes[attr] = tmp.reshape((len(obj.data.polygons), -1))
+ return face_attributes
+
def objectdata_from_VF(vertices, faces):
new_mesh = bpy.data.meshes.new("")
new_mesh.vertices.add(len(vertices))
@@ -75,6 +87,7 @@ def __init__(self, normal_mode=NormalMode.Mean,
obj=None, mesh_only=False, **kwargs
):
self.normal_mode = normal_mode
+ self.face_attributes = {}
if path is not None:
geometry = trimesh.load(path, process=False).geometry
key = list(geometry.keys())[0]
@@ -116,6 +129,7 @@ def __init__(self, normal_mode=NormalMode.Mean,
if not mesh_only:
vertex_attributes = object_to_vertex_attributes(obj)
_trimesh.vertex_attributes.update(vertex_attributes)
+ self.face_attributes.update(object_to_face_attributes(obj))
for key in kwargs:
setattr(self, key, kwargs[key])
else:
diff --git a/infinigen/tools/blendscript_import_infinigen.py b/infinigen/tools/blendscript_import_infinigen.py
index f4c419361..428ee5e14 100644
--- a/infinigen/tools/blendscript_import_infinigen.py
+++ b/infinigen/tools/blendscript_import_infinigen.py
@@ -1,3 +1,8 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
'''
@@ -25,7 +30,7 @@
from infinigen.core import init, surface
from infinigen_examples import generate_nature
-init.apply_gin_configs(Path(pwd)/'infinigen_examples/configs', ['base.gin'], skip_unknown=True)
+init.apply_gin_configs(Path(pwd)/'infinigen_examples/configs_nature', ['base.gin'], skip_unknown=True)
surface.registry.initialize_from_gin()
logging.basicConfig(
diff --git a/infinigen/tools/blendscript_path_append.py b/infinigen/tools/blendscript_path_append.py
index e7acb0f1c..06cae49a5 100644
--- a/infinigen/tools/blendscript_path_append.py
+++ b/infinigen/tools/blendscript_path_append.py
@@ -1,3 +1,9 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
import os, sys
pwd = os.getcwd()
sys.path.append(pwd)
\ No newline at end of file
diff --git a/infinigen/tools/config/demo_config.yaml b/infinigen/tools/config/demo_config.yaml
new file mode 100644
index 000000000..a655aa272
--- /dev/null
+++ b/infinigen/tools/config/demo_config.yaml
@@ -0,0 +1,12 @@
+
+scene:
+ num_static_objs: 5
+ num_rearrange_objs: 5
+ ground_size: 2.0
+ support_scene: 'scene_000'
+ support_restitution: 0.5
+ support_static_friction: 0.6
+ support_dynamic_friction: 0.4
+ camera_resolution: (1280, 720)
+ robot_base_ori: [0, 0, -90]
+
diff --git a/infinigen/tools/convert_displacement.py b/infinigen/tools/convert_displacement.py
new file mode 100644
index 000000000..2e28c108d
--- /dev/null
+++ b/infinigen/tools/convert_displacement.py
@@ -0,0 +1,124 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: David Yan
+
+import bpy
+from infinigen.core.nodes.node_wrangler import geometry_node_group_empty_new
+
+visited_nodes = []
+
+def find_connected(node, origin_socket_index, tree_origin_socket = False): #WIP, unused
+ if node in visited_nodes:
+ return
+ visited_nodes.append(node)
+
+ if node.type == "GROUP":
+ find_connected(node.node_tree.nodes["Group Output"], origin_socket_index, True)
+
+ if tree_origin_socket:
+ for link in node.inputs[origin_socket_index].links:
+ from_node = link.from_node
+ find_connected(from_node, from_node.outputs[:].index(link.from_socket))
+ else:
+ for index, _ in enumerate(node.inputs):
+ for link in node.inputs[index].links:
+ from_node = link.from_node
+ find_connected(from_node, from_node.outputs[:].index(link.from_socket))
+
+def remove_unconnected(node_tree): #WIP, unused
+ nodes = node_tree.nodes
+ for node in nodes:
+ if node not in visited_nodes:
+ nodes.remove(node)
+ continue
+ if node.type == "GROUP":
+ remove_unconnected(node.node_tree)
+
+def copy_nodes(shader_node_tree, geo_node_tree): #WIP, unused
+ shader_nodes = shader_node_tree.nodes
+ for shader_node in shader_nodes:
+ if shader_node.type == "GROUP":
+ geo_node = geo_node_tree.nodes.new("GeometryNodeGroup")
+ copy_nodes(geo_node.node_tree, shader_node.node_tree)
+ else:
+ try:
+ geo_node = geo_node_tree.nodes.new(shader_node.bl_idname)
+ geo_node.location = shader_node.location
+ geo_node.width = shader_node.width
+ except RuntimeError:
+ continue
+
+
+def bake_vertex_colors(obj):
+ bpy.context.scene.render.engine = 'CYCLES'
+ bpy.context.scene.cycles.device = "GPU"
+ bpy.context.scene.cycles.samples = 1
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = obj
+ vertColor = bpy.context.object.data.color_attributes.new(name="Displacement",domain='POINT',type='FLOAT_COLOR')
+ bpy.context.object.data.attributes.active_color = vertColor
+ bpy.ops.object.bake(type='EMIT', pass_filter={'COLOR'}, target ='VERTEX_COLORS')
+ obj.select_set(False)
+
+def create_modifier(obj, scale_val, apply_geo_modifier ):
+ modifier = obj.modifiers.new("Displacement", "NODES")
+ modifier.node_group = geometry_node_group_empty_new()
+ nodes = modifier.node_group.nodes
+ normal = nodes.new(type = 'GeometryNodeInputNormal')
+ attribute = nodes.new(type = 'GeometryNodeInputNamedAttribute')
+ attribute.data_type = "FLOAT_COLOR"
+ attribute.inputs[0].default_value = "Displacement"
+ set_pos = nodes.new(type = 'GeometryNodeSetPosition')
+ mult = nodes.new(type = 'ShaderNodeVectorMath')
+ mult.operation = 'MULTIPLY'
+ scale = nodes.new(type = 'ShaderNodeVectorMath')
+ scale.operation = 'SCALE'
+ scale.inputs["Scale"].default_value = scale_val
+ output = nodes["Group Output"]
+ input = nodes["Group Input"]
+
+ modifier.node_group.links.new(input.outputs["Geometry"], set_pos.inputs["Geometry"])
+ modifier.node_group.links.new(attribute.outputs[2], mult.inputs[0]) # index 2 must be hardcoded
+ modifier.node_group.links.new(normal.outputs["Normal"], mult.inputs[1])
+ modifier.node_group.links.new(mult.outputs["Vector"], scale.inputs["Vector"])
+ modifier.node_group.links.new(scale.outputs["Vector"], set_pos.inputs["Offset"])
+ modifier.node_group.links.new(set_pos.outputs["Geometry"], output.inputs["Geometry"])
+
+ if apply_geo_modifier:
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = obj
+ bpy.ops.object.modifier_apply(modifier="Displacement")
+ obj.select_set(False)
+
+def convert_shader_displacement(obj, apply_geo_modifier = True):
+ displaced_materials = {}
+
+ for slot in obj.material_slots:
+ mat = slot.material
+ nodes = mat.node_tree.nodes
+ if nodes.get("Displacement"):
+ scale_val = nodes["Displacement"].inputs["Scale"].default_value
+ displacement_link = nodes["Displacement"].inputs["Height"].links[0]
+ displace_socket = displacement_link.from_socket
+ bsdf_link = nodes["Material Output"].inputs["Surface"].links[0]
+ bsdf_socket = bsdf_link.from_socket
+ mat.node_tree.links.remove(displacement_link)
+ mat.node_tree.links.new(displace_socket, nodes["Material Output"].inputs["Surface"])
+ displaced_materials[mat] = bsdf_socket
+
+ if len(displaced_materials) != 0:
+ bake_vertex_colors(obj)
+ create_modifier(obj, scale_val, apply_geo_modifier)
+
+ for mat in displaced_materials:
+ mat = slot.material
+ mat.node_tree.links.remove(nodes["Material Output"].inputs["Surface"].links[0])
+ mat.node_tree.links.new(displaced_materials[mat], nodes["Material Output"].inputs["Surface"])
+
+
+
+
+
+
+
diff --git a/infinigen/tools/export.py b/infinigen/tools/export.py
index 21da43bc9..5b04bf8b9 100644
--- a/infinigen/tools/export.py
+++ b/infinigen/tools/export.py
@@ -10,12 +10,17 @@
import shutil
import subprocess
import logging
+import gin
+import math
from pathlib import Path
-FORMAT_CHOICES = ["fbx", "obj", "usdc", "usda" "stl", "ply"]
+import gin
+
+FORMAT_CHOICES = ["fbx", "obj", "usdc", "usda", "stl", "ply"]
BAKE_TYPES = {'DIFFUSE': 'Base Color', 'ROUGHNESS': 'Roughness'} # 'EMIT':'Emission' # "GLOSSY": 'Specular', 'TRANSMISSION':'Transmission' don't export
-SPECIAL_BAKE = {'METAL': 'Metallic'}
+SPECIAL_BAKE = {'METAL': 'Metallic', 'NORMAL': 'Normal'}
+ALL_BAKE = BAKE_TYPES | SPECIAL_BAKE
def apply_all_modifiers(obj):
for mod in obj.modifiers:
@@ -37,7 +42,7 @@ def realizeInstances(obj):
geo_group = mod.node_group
outputNode = geo_group.nodes['Group Output']
- logging.info(f"Realizing instances on {mod.name}")
+ logging.info(f"Realizing instances on {mod}")
link = outputNode.inputs[0].links[0]
from_socket = link.from_socket
geo_group.links.remove(link)
@@ -86,8 +91,57 @@ def handle_geo_modifiers(obj, export_usd):
if not export_usd:
realizeInstances(obj)
-
-def clean_names():
+
+def split_glass_mats():
+ split_objs = []
+ for obj in bpy.data.objects:
+ if any(exclude in obj.name for exclude in ['BowlFactory', 'CupFactory', 'OvenFactory', 'BottleFactory']):
+ continue
+ for slot in obj.material_slots:
+ mat = slot.material
+ if mat is None:
+ continue
+ if ('shader_glass' in mat.name or 'shader_lamp_bulb' in mat.name) and len(obj.material_slots) >= 2:
+ logging.info(f'Splitting {obj}')
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = obj
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.separate(type='MATERIAL')
+ bpy.ops.object.mode_set(mode='OBJECT')
+ obj.select_set(False)
+ split_objs.append(obj.name)
+ break
+
+ matches = [obj for split_obj in split_objs for obj in bpy.data.objects if split_obj in obj.name]
+ for match in matches:
+ mat = match.material_slots[0].material
+ if mat is None:
+ continue
+ if ('shader_glass' in mat.name or 'shader_lamp_bulb' in mat.name):
+ match.name = f'{match.name}_SPLIT_GLASS'
+
+def clean_names(obj = None):
+ if obj is not None:
+ obj.name = (obj.name).replace(' ','_')
+ obj.name = (obj.name).replace('.','_')
+
+ if obj.type == 'MESH':
+ for uv_map in obj.data.uv_layers:
+ uv_map.name = uv_map.name.replace('.', '_')
+
+ for mat in bpy.data.materials:
+ if (mat is None): continue
+ mat.name = (mat.name).replace(' ','_')
+ mat.name = (mat.name).replace('.','_')
+
+ for slot in obj.material_slots:
+ mat = slot.material
+ if (mat is None):
+ continue
+ mat.name = (mat.name).replace(' ','_')
+ mat.name = (mat.name).replace('.','_')
+ return
+
for obj in bpy.data.objects:
obj.name = (obj.name).replace(' ','_')
obj.name = (obj.name).replace('.','_')
@@ -101,32 +155,76 @@ def clean_names():
mat.name = (mat.name).replace(' ','_')
mat.name = (mat.name).replace('.','_')
-def remove_obj_parents():
- for obj in bpy.data.objects:
- world_loc = obj.matrix_world.to_translation()
+def remove_obj_parents(obj = None):
+ if obj is not None :
+ old_location = obj.matrix_world.to_translation()
+ obj.parent = None
+ obj.matrix_world.translation = old_location
+ return
+
+ for obj in bpy.data.objects:
+ old_location = obj.matrix_world.to_translation()
obj.parent = None
- obj.matrix_world.translation = world_loc
+ obj.matrix_world.translation = old_location
+
+def delete_objects():
+ logging.info("Deleting placeholders collection")
+ collection_name = "placeholders"
+ collection = bpy.data.collections.get(collection_name)
+
+ if collection:
+ for scene in bpy.data.scenes:
+ if collection.name in scene.collection.children:
+ scene.collection.children.unlink(collection)
+
+ for obj in collection.objects:
+ bpy.data.objects.remove(obj, do_unlink=True)
+
+ def delete_child_collections(parent_collection):
+ for child_collection in parent_collection.children:
+ delete_child_collections(child_collection)
+ bpy.data.collections.remove(child_collection)
+
+ delete_child_collections(collection)
+ bpy.data.collections.remove(collection)
+
+ if bpy.data.objects.get("Grid"):
+ bpy.data.objects.remove(bpy.data.objects["Grid"], do_unlink=True)
+
+ if bpy.data.objects.get("atmosphere"):
+ bpy.data.objects.remove(bpy.data.objects["atmosphere"], do_unlink=True)
+
+ if bpy.data.objects.get("KoleClouds"):
+ bpy.data.objects.remove(bpy.data.objects["KoleClouds"], do_unlink=True)
+
+def rename_all_meshes(obj = None):
+ if obj is not None:
+ if obj.data and obj.data.users == 1:
+ obj.data.name = obj.name
+ return
+
+ for obj in bpy.data.objects:
+ if obj.data and obj.data.users == 1:
+ obj.data.name = obj.name
def update_visibility():
outliner_area = next(a for a in bpy.context.screen.areas if a.type == 'OUTLINER')
space = outliner_area.spaces[0]
space.show_restrict_column_viewport = True # Global visibility (Monitor icon)
- revealed_collections = []
- hidden_objs = []
+ collection_view = {}
+ obj_view = {}
for collection in bpy.data.collections:
+ collection_view[collection] = collection.hide_render
collection.hide_viewport = False #reenables viewports for all
- # enables renders for all collections
- if collection.hide_render:
- collection.hide_render = False
- revealed_collections.append(collection)
+ collection.hide_render = False # enables renders for all collections
+ # disables viewports and renders for all objs
for obj in bpy.data.objects:
+ obj_view[obj] = obj.hide_render
obj.hide_viewport = True
- if not obj.hide_render:
- hidden_objs.append(obj)
- obj.hide_render = True
-
- return revealed_collections, hidden_objs
+ obj.hide_render = True
+
+ return collection_view, obj_view
def uv_unwrap(obj):
obj.select_set(True)
@@ -139,7 +237,7 @@ def uv_unwrap(obj):
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
try:
- bpy.ops.uv.smart_project()
+ bpy.ops.uv.smart_project(angle_limit=0.7)
except RuntimeError:
logging.info("UV Unwrap failed, skipping mesh")
bpy.ops.object.mode_set(mode='OBJECT')
@@ -166,7 +264,7 @@ def apply_baked_tex(obj, paramDict={}):
if "ExportUV" not in uv_layer.name:
logging.info(f"Removed extraneous UV Layer {uv_layer}")
obj.data.uv_layers.remove(uv_layer)
-
+
for slot in obj.material_slots:
mat = slot.material
if (mat is None):
@@ -176,8 +274,7 @@ def apply_baked_tex(obj, paramDict={}):
logging.info("Reapplying baked texs on " + mat.name)
# delete all nodes except baked nodes and bsdf
- excludedNodes = [type + '_node' for type in BAKE_TYPES]
- excludedNodes.extend([type + '_node' for type in SPECIAL_BAKE])
+ excludedNodes = [type + '_node' for type in ALL_BAKE]
excludedNodes.extend(['Material Output','Principled BSDF'])
for n in nodes:
if n.name not in excludedNodes:
@@ -201,37 +298,47 @@ def apply_baked_tex(obj, paramDict={}):
# create the new shader node links
links.new(output.inputs[0], principled_bsdf_node.outputs[0])
- for type in BAKE_TYPES:
- if not nodes.get(type + '_node'): continue
- tex_node = nodes[type + '_node']
- links.new(principled_bsdf_node.inputs[BAKE_TYPES[type]], tex_node.outputs[0])
- for type in SPECIAL_BAKE:
+ for type in ALL_BAKE:
if not nodes.get(type + '_node'): continue
tex_node = nodes[type + '_node']
- links.new(principled_bsdf_node.inputs[BAKE_TYPES[type]], tex_node.outputs[0])
-
+ if type == 'NORMAL':
+ normal_node = nodes.new('ShaderNodeNormalMap')
+ links.new(normal_node.inputs['Color'], tex_node.outputs[0])
+ links.new(principled_bsdf_node.inputs[ALL_BAKE[type]], normal_node.outputs[0])
+ continue
+ links.new(principled_bsdf_node.inputs[ALL_BAKE[type]], tex_node.outputs[0])
+
# bring back cleared param values
if mat.name in paramDict:
principled_bsdf_node.inputs['Metallic'].default_value = paramDict[mat.name]['Metallic']
principled_bsdf_node.inputs['Sheen'].default_value = paramDict[mat.name]['Sheen']
principled_bsdf_node.inputs['Clearcoat'].default_value = paramDict[mat.name]['Clearcoat']
-def create_glass_shader(node_tree):
+def create_glass_shader(node_tree, export_usd):
nodes = node_tree.nodes
- color = nodes['Glass BSDF'].inputs[0].default_value
- roughness = nodes['Glass BSDF'].inputs[1].default_value
- ior = nodes['Glass BSDF'].inputs[2].default_value
+ if nodes.get('Glass BSDF'):
+ color = nodes['Glass BSDF'].inputs[0].default_value
+ roughness = nodes['Glass BSDF'].inputs[1].default_value
+ ior = nodes['Glass BSDF'].inputs[2].default_value
+
if nodes.get('Principled BSDF'):
nodes.remove(nodes['Principled BSDF'])
principled_bsdf_node = nodes.new('ShaderNodeBsdfPrincipled')
- principled_bsdf_node.inputs['Base Color'].default_value = color
- principled_bsdf_node.inputs['Roughness'].default_value = roughness
- principled_bsdf_node.inputs['IOR'].default_value = ior
+
+ if nodes.get('Glass BSDF'):
+ principled_bsdf_node.inputs['Base Color'].default_value = color
+ principled_bsdf_node.inputs['Roughness'].default_value = roughness
+ principled_bsdf_node.inputs['IOR'].default_value = ior
+ else:
+ principled_bsdf_node.inputs['Roughness'].default_value = 0
+
principled_bsdf_node.inputs['Transmission'].default_value = 1
+ if export_usd:
+ principled_bsdf_node.inputs['Alpha'].default_value = 0
node_tree.links.new(principled_bsdf_node.outputs[0], nodes['Material Output'].inputs[0])
-def process_glass_materials(obj):
+def process_glass_materials(obj, export_usd):
for slot in obj.material_slots:
mat = slot.material
if (mat is None or not mat.use_nodes): continue
@@ -239,15 +346,20 @@ def process_glass_materials(obj):
outputNode = nodes['Material Output']
if nodes.get('Glass BSDF'):
if outputNode.inputs[0].links[0].from_node.bl_idname == 'ShaderNodeBsdfGlass':
- create_glass_shader(mat.node_tree)
+ logging.info(f"Creating glass material on {obj.name}")
else:
logging.info(f"Non-trivial glass material on {obj.name}, material export will be inaccurate")
+ create_glass_shader(mat.node_tree, export_usd)
+ elif 'glass' in mat.name or 'shader_lamp_bulb' in mat.name:
+ logging.info(f"Creating glass material on {obj.name}")
+ create_glass_shader(mat.node_tree, export_usd)
def bake_pass(
obj,
dest: Path,
img_size,
- bake_type,
+ bake_type,
+ export_usd
):
img = bpy.data.images.new(f'{obj.name}_{bake_type}',img_size,img_size)
@@ -275,15 +387,22 @@ def bake_pass(
img_node = nodes.new('ShaderNodeTexImage')
img_node.name = f'{bake_type}_node'
img_node.image = img
+ img_node.select = True
nodes.active = img_node
img_node.select = True
- if len(output.inputs[0].links) != 0:
- surface_node = output.inputs[0].links[0].from_node
- if surface_node.bl_idname == 'ShaderNodeBsdfPrincipled' and len(surface_node.inputs[BAKE_TYPES[bake_type]].links) == 0: # trivial bsdf graph
- logging.info(f"{mat.name} has no procedural input for {bake_type}, not using baked textures")
- bake_exclude_mats[mat] = img_node
- continue
+ if len(output.inputs[0].links) == 0:
+ logging.info(f"{mat.name} has no surface output, not using baked textures")
+ bake_exclude_mats[mat] = img_node
+ continue
+
+ surface_node = output.inputs[0].links[0].from_node
+ if (bake_type in ALL_BAKE and surface_node.bl_idname == 'ShaderNodeBsdfPrincipled' and
+ len(surface_node.inputs[ALL_BAKE[bake_type]].links) == 0): # trivial bsdf graph
+
+ logging.info(f"{mat.name} has no procedural input for {bake_type}, not using baked textures")
+ bake_exclude_mats[mat] = img_node
+ continue
bake_obj = True
@@ -296,7 +415,8 @@ def bake_pass(
logging.info(f'Baking {bake_type} pass')
bpy.ops.object.bake(type=internal_bake_type, pass_filter={'COLOR'}, save_mode='EXTERNAL')
img.filepath_raw = str(file_path)
- img.save()
+ if not export_usd:
+ img.save()
logging.info(f"Saving to {file_path}")
else:
logging.info(f"No necessary materials to bake on {obj.name}, skipping bake")
@@ -304,7 +424,7 @@ def bake_pass(
for mat, img_node in bake_exclude_mats.items():
mat.node_tree.nodes.remove(img_node)
-def bake_metal(obj, dest, img_size): # metal baking is not really set up for node graphs w/ 2 mixed BSDFs.
+def bake_metal(obj, dest, img_size, export_usd): # metal baking is not really set up for node graphs w/ 2 mixed BSDFs.
metal_map_mats = []
for slot in obj.material_slots:
mat = slot.material
@@ -325,21 +445,42 @@ def bake_metal(obj, dest, img_size): # metal baking is not really set up for nod
metal_map_mats.append(mat)
if len(metal_map_mats) != 0:
- bake_pass(obj, dest, img_size, 'METAL')
+ bake_pass(obj, dest, img_size, 'METAL', export_usd)
for mat in metal_map_mats:
+ nodes = mat.node_tree.nodes
+ outputNode = nodes["Material Output"]
+ principled_bsdf_node = nodes['Principled BSDF']
links.remove(outputNode.inputs[0].links[0])
links.new(outputNode.inputs[0], principled_bsdf_node.outputs[0])
+
+def bake_normals(obj, dest, img_size, export_usd):
+ bake_obj = False
+ for slot in obj.material_slots:
+ mat = slot.material
+ if (mat is None or not mat.use_nodes): continue
+ nodes = mat.node_tree.nodes
+ if nodes.get('Material Output'):
+ outputNode = nodes['Material Output']
+ else: continue
+
+ if len(outputNode.inputs['Displacement'].links) != 0:
+ bake_obj = True
+
+ if bake_obj:
+ bake_pass(obj, dest, img_size, 'NORMAL', export_usd)
+
def remove_params(mat, node_tree):
- paramDict = {}
nodes = node_tree.nodes
+ paramDict = {}
if nodes.get('Material Output'):
output = nodes['Material Output']
elif nodes.get('Group Output'):
output = nodes['Group Output']
else:
raise ValueError("Could not find material output node")
+
if nodes.get('Principled BSDF') and output.inputs[0].links[0].from_node.bl_idname == 'ShaderNodeBsdfPrincipled':
principled_bsdf_node = nodes['Principled BSDF']
metal = principled_bsdf_node.inputs['Metallic'].default_value # store metallic value and set to 0
@@ -349,21 +490,75 @@ def remove_params(mat, node_tree):
principled_bsdf_node.inputs['Metallic'].default_value = 0
principled_bsdf_node.inputs['Sheen'].default_value = 0
principled_bsdf_node.inputs['Clearcoat'].default_value = 0
+ return paramDict
+
+ for node in nodes:
+ if node.type == 'GROUP':
+ paramDict = remove_params(mat, node.node_tree)
+ if len(paramDict) != 0:
+ return paramDict
+
return paramDict
def process_interfering_params(obj):
for slot in obj.material_slots:
mat = slot.material
if (mat is None or not mat.use_nodes): continue
- paramDict = remove_params(mat, mat.node_tree)
- if len(paramDict) == 0:
- for node in mat.node_tree.nodes: # only handles one level of sub-groups
- if node.type == 'GROUP':
- paramDict = remove_params(mat, node.node_tree)
-
+ paramDict = remove_params(mat, mat.node_tree)
return paramDict
-def bake_object(obj, dest, img_size):
+def skipBake(obj):
+ if not obj.data.materials:
+ logging.info("No material on mesh, skipping...")
+ return True
+
+ if len(obj.data.vertices) == 0:
+ logging.info("Mesh has no vertices, skipping ...")
+ return True
+
+ return False
+
+def triangulate_meshes():
+ logging.info("Triangulating Meshes")
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'MESH':
+ view_state = obj.hide_viewport
+ obj.hide_viewport = False
+ bpy.context.view_layer.objects.active = obj
+ obj.select_set(True)
+ bpy.ops.object.mode_set(mode='EDIT')
+ bpy.ops.mesh.select_all(action='SELECT')
+ logging.info(f"Triangulating {obj}")
+ bpy.ops.mesh.quads_convert_to_tris()
+ bpy.ops.object.mode_set(mode='OBJECT')
+ obj.select_set(False)
+ obj.hide_viewport = view_state
+
+def adjust_wattages():
+ logging.info("Adjusting light wattage")
+ for obj in bpy.context.scene.objects:
+ if obj.type == 'LIGHT' and obj.data.type == 'POINT':
+ light = obj.data
+ if hasattr(light, 'energy') and hasattr(light, 'shadow_soft_size'):
+ X = light.energy
+ r = light.shadow_soft_size
+ # candelas * 1000 / (4 * math.pi * r**2). additionally units come out of blender at 1/100 scale
+ new_wattage = (X * 20 / (4 * math.pi)) * 1000 / (4 * math.pi * r**2) * 100
+ light.energy = new_wattage
+
+def set_center_of_mass():
+ logging.info("Resetting center of mass of objects")
+ for obj in bpy.context.scene.objects:
+ if not obj.hide_render:
+ view_state = obj.hide_viewport
+ obj.hide_viewport = False
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = obj
+ bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
+ obj.select_set(False)
+ obj.hide_viewport = view_state
+
+def bake_object(obj, dest, img_size, export_usd):
if not uv_unwrap(obj):
return
@@ -375,30 +570,20 @@ def bake_object(obj, dest, img_size):
if mat is not None:
slot.material = mat.copy() # we duplicate in the case of distinct meshes sharing materials
- process_glass_materials(obj)
-
- bake_metal(obj, dest, img_size)
+ process_glass_materials(obj, export_usd)
+
+ bake_metal(obj, dest, img_size, export_usd)
+ bake_normals(obj, dest, img_size, export_usd)
paramDict = process_interfering_params(obj)
for bake_type in BAKE_TYPES:
- bake_pass(obj, dest, img_size, bake_type)
+ bake_pass(obj, dest, img_size, bake_type, export_usd)
apply_baked_tex(obj, paramDict)
obj.select_set(False)
-def skipBake(obj):
- if not obj.data.materials:
- logging.info("No material on mesh, skipping...")
- return True
-
- if len(obj.data.vertices) == 0:
- logging.info("Mesh has no vertices, skipping ...")
- return True
-
- return False
-
def bake_scene(folderPath: Path, image_res, vertex_colors, export_usd):
for obj in bpy.data.objects:
@@ -409,23 +594,24 @@ def bake_scene(folderPath: Path, image_res, vertex_colors, export_usd):
logging.info("Not mesh, skipping ...")
continue
- if skipBake(obj): continue
+ if skipBake(obj):
+ continue
if format == "stl":
continue
-
+
obj.hide_render = False
obj.hide_viewport = False
if vertex_colors:
bakeVertexColors(obj)
else:
- bake_object(obj, folderPath, image_res)
+ bake_object(obj, folderPath, image_res, export_usd)
obj.hide_render = True
obj.hide_viewport = True
-def run_export(exportPath: Path, format: str, vertex_colors: bool, individual_export: bool):
+def run_blender_export(exportPath: Path, format: str, vertex_colors: bool, individual_export: bool):
assert exportPath.parent.exists()
exportPath = str(exportPath)
@@ -442,11 +628,21 @@ def run_export(exportPath: Path, format: str, vertex_colors: bool, individual_ex
else:
bpy.ops.export_scene.fbx(filepath = exportPath, path_mode='COPY', embed_textures = True, use_selection=individual_export)
- if format == "stl": bpy.ops.export_mesh.stl(filepath = exportPath, use_selection = individual_export)
-
- if format == "ply": bpy.ops.wm.ply_export(filepath = exportPath, export_selected_objects = individual_export)
-
- if format in ["usda", "usdc"]: bpy.ops.wm.usd_export(filepath = exportPath, export_textures=True, use_instancing=True, selected_objects_only=individual_export)
+ if format == "stl":
+ bpy.ops.export_mesh.stl(filepath = exportPath, use_selection = individual_export)
+
+ if format == "ply":
+ bpy.ops.wm.ply_export(filepath = exportPath, export_selected_objects = individual_export)
+
+ if format in ["usda", "usdc"]:
+ bpy.ops.wm.usd_export(
+ filepath=exportPath,
+ export_textures=True,
+ #use_instancing=True,
+ overwrite_textures=True,
+ selected_objects_only=individual_export,
+ root_prim_path='/World'
+ )
def export_scene(
input_blend: Path,
@@ -455,28 +651,98 @@ def export_scene(
task_uniqname=None,
**kwargs,
):
-
bpy.ops.wm.open_mainfile(filepath=str(input_blend))
-
- folder = output_folder/input_blend.name
+ folder = output_folder/f"export_{input_blend.name}"
folder.mkdir(exist_ok=True, parents=True)
result = export_curr_scene(folder, **kwargs)
-
+
if pipeline_folder is not None and task_uniqname is not None :
(pipeline_folder / "logs" / f"FINISH_{task_uniqname}").touch()
return result
+# side effects: will remove parents of inputted obj and clean its name, hides viewport of all objects
+def export_single_obj(obj: bpy.types.Object, output_folder: Path, format='usdc', image_res= 1024, vertex_colors=False):
+
+ export_usd = format in ["usda", "usdc"]
+
+ export_folder = output_folder
+ export_folder.mkdir(exist_ok=True)
+ export_file = export_folder/output_folder.with_suffix(f'.{format}').name
+
+ logging.info(f"Exporting to directory {export_folder=}")
+
+ remove_obj_parents(obj)
+ rename_all_meshes(obj)
+
+ collection_views, obj_views = update_visibility()
+
+ bpy.context.scene.render.engine = 'CYCLES'
+ bpy.context.scene.cycles.device = 'GPU'
+ bpy.context.scene.cycles.samples = 1 # choose render sample
+ # Set the tile size
+ bpy.context.scene.cycles.tile_x = image_res
+ bpy.context.scene.cycles.tile_y = image_res
+
+ if obj.type != 'MESH' or obj not in list(bpy.context.view_layer.objects):
+ raise ValueError("Object not mesh")
+
+ if export_usd:
+ apply_all_modifiers(obj)
+ else:
+ realizeInstances(obj)
+ apply_all_modifiers(obj)
+
+ if not skipBake(obj) and format != "stl":
+ if vertex_colors:
+ bakeVertexColors(obj)
+ else:
+ obj.hide_render = False
+ obj.hide_viewport = False
+ bake_object(obj, export_folder/'textures', image_res, export_usd)
+ obj.hide_render = True
+ obj.hide_viewport = True
+
+ for collection, status in collection_views.items():
+ collection.hide_render = status
+
+ for obj, status in obj_views.items():
+ obj.hide_render = status
+
+ clean_names(obj)
+
+ old_loc = obj.location.copy()
+ obj.location = (0, 0, 0)
+
+ if obj.type != 'MESH' or obj.hide_render or len(obj.data.vertices) == 0 or obj not in list(bpy.context.view_layer.objects):
+ raise ValueError("Object is not mesh or hidden from render")
+
+ export_subfolder = export_folder/obj.name
+ export_subfolder.mkdir(exist_ok=True)
+ export_file = export_subfolder/f'{obj.name}.{format}'
+
+ logging.info(f"Exporting file to {export_file=}")
+ obj.hide_viewport = False
+ obj.select_set(True)
+ run_blender_export(export_file, format, vertex_colors, individual_export=True)
+ obj.select_set(False)
+ obj.location = old_loc
+
+ return export_file
+
+@gin.configurable
def export_curr_scene(
output_folder: Path,
- format: str,
- image_res: int,
+ format='usdc',
+ image_res= 1024,
vertex_colors=False,
individual_export=False,
+ omniverse_export=False,
pipeline_folder=None,
task_uniqname=None
) -> Path:
-
+
+
export_usd = format in ["usda", "usdc"]
export_folder = output_folder
@@ -484,12 +750,13 @@ def export_curr_scene(
export_file = export_folder/output_folder.with_suffix(f'.{format}').name
logging.info(f"Exporting to directory {export_folder=}")
-
- # remove grid
- if bpy.data.objects.get("Grid"):
- bpy.data.objects.remove(bpy.data.objects["Grid"], do_unlink=True)
remove_obj_parents()
+ delete_objects()
+ triangulate_meshes()
+ if omniverse_export:
+ split_glass_mats()
+ rename_all_meshes()
scatter_cols = []
if export_usd:
@@ -507,23 +774,20 @@ def export_curr_scene(
# if obj.type == 'MESH' and len(obj.data.polygons) == 0:
# if scatter_cols is not None:
# if any(x in scatter_cols for x in obj.users_collection):
- # continue
+ # continue
# logging.info(f"{obj.name} has no faces, removing...")
# bpy.data.objects.remove(obj, do_unlink=True)
- revealed_collections, hidden_objs = update_visibility()
+ collection_views, obj_views = update_visibility()
for obj in bpy.data.objects:
if obj.type != 'MESH' or obj not in list(bpy.context.view_layer.objects):
continue
- viewport_status = obj.hide_viewport
- obj.hide_viewport = False
if export_usd:
apply_all_modifiers(obj)
else:
realizeInstances(obj)
apply_all_modifiers(obj)
- obj.hide_viewport = viewport_status
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.device = 'GPU'
@@ -540,16 +804,26 @@ def export_curr_scene(
export_usd=export_usd
)
- for collection in revealed_collections:
- logging.info(f"Hiding collection {collection.name} from render")
- collection.hide_render = True
-
- for obj in hidden_objs:
- logging.info(f"Unhiding object {obj.name} from render")
- obj.hide_render = False
+ for collection, status in collection_views.items():
+ collection.hide_render = status
+ for obj, status in obj_views.items():
+ obj.hide_render = status
+
clean_names()
+ for obj in bpy.data.objects:
+ obj.hide_viewport = obj.hide_render
+
+ if omniverse_export:
+ adjust_wattages()
+ set_center_of_mass()
+ # remove 0 polygon meshes
+ for obj in bpy.data.objects:
+ if obj.type == 'MESH' and len(obj.data.polygons) == 0:
+ logging.info(f"{obj.name} has no faces, removing...")
+ bpy.data.objects.remove(obj, do_unlink=True)
+
if individual_export:
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.location_clear() # send all objects to (0,0,0)
@@ -565,22 +839,24 @@ def export_curr_scene(
logging.info(f"Exporting file to {export_file=}")
obj.hide_viewport = False
obj.select_set(True)
- run_export(export_file, format, vertex_colors, individual_export)
+ run_blender_export(export_file, format, vertex_colors, individual_export)
obj.select_set(False)
else:
logging.info(f"Exporting file to {export_file=}")
- run_export(export_file, format, vertex_colors, individual_export)
+ run_blender_export(export_file, format, vertex_colors, individual_export)
- return export_folder
+ return export_file
def main(args):
-
args.output_folder.mkdir(exist_ok=True)
- logging.basicConfig(level=logging.DEBUG)
+ logging.basicConfig(filename= args.output_folder/'export_logs.log', level=logging.DEBUG, filemode = "w+")
targets = sorted(list(args.input_folder.iterdir()))
for blendfile in targets:
+ if blendfile.stem == 'solve_state':
+ shutil.copy(blendfile, args.output_folder/'solve_state.json')
+
if not blendfile.suffix == '.blend':
print(f'Skipping non-blend file {blendfile}')
continue
@@ -592,10 +868,11 @@ def main(args):
image_res=args.resolution,
vertex_colors=args.vertex_colors,
individual_export=args.individual,
+ omniverse_export=args.omniverse
)
# wanted to use shutil here but kept making corrupted files
- subprocess.call(['zip', '-r', str(folder.absolute().with_suffix('.zip')), str(folder.absolute())])
-
+ subprocess.call(['zip', '-r', str(folder.with_suffix('.zip')), str(folder)])
+
bpy.ops.wm.quit_blender()
def make_args():
@@ -609,6 +886,7 @@ def make_args():
parser.add_argument('-v', '--vertex_colors', action = 'store_true')
parser.add_argument('-r', '--resolution', default= 1024, type=int)
parser.add_argument('-i', '--individual', action = 'store_true')
+ parser.add_argument('-o', '--omniverse', action = 'store_true')
args = parser.parse_args()
diff --git a/infinigen/tools/indoor_profile.py b/infinigen/tools/indoor_profile.py
new file mode 100644
index 000000000..b55ad020e
--- /dev/null
+++ b/infinigen/tools/indoor_profile.py
@@ -0,0 +1,83 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: David Yan
+
+from pathlib import Path
+import re
+from datetime import timedelta
+from collections import defaultdict
+import argparse
+import pandas as pd
+from tabulate import tabulate
+
+
+'''
+The following function s attributed to FObersteiner from Stack Overflow at https://stackoverflow.com/a/64662985
+and is licensed under CC-BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/deed.en#ref-appropriate-credit).
+David Yan used this code WITHOUT modification.
+'''
+def td_to_str(td):
+ """
+ convert a timedelta object td to a string in HH:MM:SS format.
+ """
+ if (pd.isnull(td)):
+ return td
+ hours, remainder = divmod(td.total_seconds(), 3600)
+ minutes, seconds = divmod(remainder, 60)
+ return f'{int(hours):02}:{int(minutes):02}:{int(seconds):02}'
+
+def main(dir : Path):
+ coarse_data = defaultdict(list)
+ render_data = defaultdict(list)
+ with open(dir/"finished_seeds.txt") as f:
+ seeds = f.read().splitlines()
+
+ for seed in seeds:
+ try:
+ coarse_log = open(dir/seed/'logs'/'coarse.err').read()
+ render_log = open(next((dir/seed/"logs").glob('shortrender*.err'))).read()
+ except:
+ continue
+
+ for name, h, m, s in re.findall(r'\[INFO\] \| \[(.*?)\] finished in ([0-9]+):([0-9]+):([0-9]+)', coarse_log):
+ timedelta_obj = timedelta(hours=int(h), minutes=int(m), seconds=int(s))
+ if (timedelta_obj.total_seconds() < 1): continue
+ coarse_data[name].append(timedelta_obj)
+
+ for name, h, m, s in re.findall(r'\[INFO\] \| \[(.*?)\] finished in ([0-9]+):([0-9]+):([0-9]+)', render_log):
+ timedelta_obj = timedelta(hours=int(h), minutes=int(m), seconds=int(s))
+ if (timedelta_obj.total_seconds() < 1): continue
+ render_data[name].append(timedelta_obj)
+
+ coarse_stats = make_stats(pd.DataFrame.from_dict(coarse_data, orient='index'))
+ render_stats = make_stats(pd.DataFrame.from_dict(render_data, orient='index'))
+
+ for column in coarse_stats:
+ coarse_stats[column] = coarse_stats[column].dt.round('1s').map(lambda x: td_to_str(x))
+
+ for column in coarse_stats:
+ render_stats[column] = render_stats[column].dt.round('1s').map(lambda x: td_to_str(x))
+
+ print(coarse_stats.sort_values("median", ascending=False))
+ print(render_stats.sort_values("median", ascending=False))
+
+
+def make_stats(data_df):
+ stats = pd.DataFrame()
+ stats['mean'] = data_df.mean(axis=1)
+ stats['median'] = data_df.median(axis=1)
+ stats['90%'] = data_df.quantile(0.9, axis=1)
+ stats['95%'] = data_df.quantile(0.95, axis=1)
+ stats['99%'] = data_df.quantile(0.99, axis=1)
+ return stats
+
+def make_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-d', '--dir', type=Path)
+ args = parser.parse_args()
+ return args
+
+if __name__ == '__main__':
+ args = make_args()
+ main(args.dir)
diff --git a/infinigen/tools/isaac_sim.py b/infinigen/tools/isaac_sim.py
new file mode 100644
index 000000000..0f704b2a1
--- /dev/null
+++ b/infinigen/tools/isaac_sim.py
@@ -0,0 +1,171 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: David Yan, Beining Han
+
+# Acknowledgement: This file draws inspiration from https://docs.omniverse.nvidia.com/isaacsim/latest/index.html
+
+import numpy as np
+
+from omni.isaac.kit import SimulationApp
+CONFIG = {"renderer": "RayTracedLighting", "headless": False}
+simulation_app = SimulationApp(launch_config=CONFIG)
+
+import omni
+import json
+from omni.isaac.core import World
+from pxr import Usd,UsdGeom, UsdLux, Sdf
+from omni.isaac.core.utils.prims import create_prim
+from omni.isaac.core.utils.nucleus import get_assets_root_path
+from omni.isaac.core.prims import XFormPrim
+from omni.kit.commands import execute as omni_exec
+from omni.isaac.core.utils.extensions import enable_extension
+enable_extension("omni.isaac.examples")
+from omni.isaac.core.utils.nucleus import get_assets_root_path
+from omni.isaac.core.utils.types import ArticulationAction
+from omni.isaac.core.controllers import BaseController
+from omni.isaac.wheeled_robots.robots import WheeledRobot
+from omni.physx.scripts import utils
+
+class RobotController(BaseController):
+ def __init__(self):
+ super().__init__(name="robot_controller")
+
+ def forward(self):
+ return ArticulationAction(joint_velocities=[2,2])
+
+class InfinigenIsaacScene(object):
+ def __init__(self, cfg):
+ self.cfg = cfg
+ self.world = World(stage_units_in_meters=1.0, backend='numpy', physics_dt=1/400.)
+ self.world._physics_context.set_gravity(-9.8)
+ self.scene = self.world.scene
+ self._support = None
+ self.setup_scene()
+
+ def setup_scene(self):
+ self._add_infinigen_scene()
+ self._add_lighting()
+ self._add_robot()
+
+ def _add_infinigen_scene(self):
+ create_prim(prim_path="/World/Support",
+ usd_path=self.cfg.scene_path,
+ semantic_label='scene')
+ self._support = XFormPrim(prim_path="/World/Support", name="Support")
+
+ stage = omni.usd.get_context().get_stage()
+
+ prims = [prim for prim in stage.Traverse() if prim.IsA(UsdGeom.Mesh)]
+ if self.cfg.json_path is None:
+ for prim in prims:
+ utils.setStaticCollider(prim)
+ self.scene.add(self._support)
+ return
+
+ with open(self.cfg.json_path) as json_file:
+ relations = json.load(json_file)["objs"]
+
+ obj_to_target = {}
+ for key, value in relations.items():
+ obj = value.get("obj")
+ if obj:
+ obj_to_target[obj.replace('(', '_').replace(')', '_').replace('.', '_')] = key
+
+ for prim in prims:
+ prim_name = prim.GetName()
+ target = obj_to_target.get(prim_name)
+
+ if 'SPLIT' in prim_name:
+ do_not_cast_shadows = prim.CreateAttribute('primvars:doNotCastShadows', Sdf.ValueTypeNames.Bool)
+ do_not_cast_shadows.Set(True)
+
+ if 'terrain' in prim_name:
+ continue
+
+ if not target:
+ utils.setStaticCollider(prim)
+ continue
+
+ if any(x["relation"]["relation_type"] == "StableAgainst" and "Subpart(wall)" in x["relation"].get("parent_tags") or "Subpart(ceiling)" in x["relation"].get("parent_tags") for x in relations[target]["relations"]):
+ utils.setStaticCollider(prim)
+ else:
+ utils.setRigidBody(prim, 'convexDecomposition', False)
+
+ self.scene.add(self._support)
+
+ def _add_lighting(self):
+ omni_exec(
+ "CreatePrim",
+ prim_path='/World/DomeLight',
+ prim_type="DomeLight",
+ select_new_prim=False,
+ attributes={
+ UsdLux.Tokens.inputsIntensity: 5000,
+ UsdLux.Tokens.inputsColor: (0.7, 0.88, 1.0)
+ },
+ create_default_xform=True,
+ )
+ omni_exec(
+ "CreatePrim",
+ prim_path='/World/DistantLight',
+ prim_type="DistantLight",
+ select_new_prim=False,
+ attributes={
+ UsdLux.Tokens.inputsIntensity: 8000
+ },
+ create_default_xform=True,
+ )
+
+ def _get_camera_loc(self):
+ stage = omni.usd.get_context().get_stage()
+ prim = stage.GetPrimAtPath("/World/Support/CameraRigs_0_0")
+ xform = UsdGeom.Xformable(prim)
+ transform_matrix = xform.ComputeLocalToWorldTransform(Usd.TimeCode.Default())
+ translation = transform_matrix.ExtractTranslation()
+ translation[2] = 0
+ return translation, [1, 0, 0, 0]
+
+ def _add_robot(self):
+ robot_path = get_assets_root_path() + "/Isaac/Robots/Jetbot/jetbot.usd"
+ init_pos, _ = self._get_camera_loc()
+ init_pos[-1] += 0.3
+ self.robot = self.scene.add(
+ WheeledRobot(
+ prim_path="/World/Robot",
+ name="Robot",
+ wheel_dof_names=["left_wheel_joint", "right_wheel_joint"],
+ create_robot=True,
+ usd_path=robot_path,
+ position=init_pos
+ )
+ )
+ self.robot.set_local_scale(np.array([4, 4, 4]))
+ self.controller = RobotController()
+ self.world.reset()
+
+ def apply_action(self):
+ self.robot.apply_action(self.controller.forward())
+
+ def reset(self):
+ self.world.reset()
+
+ def run(self):
+ self.world.reset()
+ while simulation_app.is_running():
+ self.apply_action()
+ self.world.step(render=True)
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--scene-path', type=str)
+ parser.add_argument('--json-path', type=str)
+ args = parser.parse_args()
+
+ scene = InfinigenIsaacScene(args)
+
+ scene.reset()
+ scene.run()
+ simulation_app.close()
+
diff --git a/infinigen/tools/perceptual/analysis.ipynb b/infinigen/tools/perceptual/analysis.ipynb
new file mode 100644
index 000000000..cd1641f06
--- /dev/null
+++ b/infinigen/tools/perceptual/analysis.ipynb
@@ -0,0 +1,411 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import statsmodels.api as sm\n",
+ "\n",
+ "def A_count_proportion(df):\n",
+ " A_count = 0\n",
+ " B_count = 0\n",
+ " for index, _ in df.iterrows():\n",
+ " row = df.iloc[index]\n",
+ " if \"Program A\" in row['Answer.category.label']:\n",
+ " A_count += 1\n",
+ " elif \"Program B\" in row['Answer.category.label']:\n",
+ " B_count += 1\n",
+ " number_of_successes = A_count # number of times program A (or B) was chosen as more realistic\n",
+ " n = (A_count+B_count) # total number of submissions\n",
+ "\n",
+ " # Confidence level: 99%\n",
+ " confidence_level = 0.99\n",
+ " alpha = 1 - confidence_level\n",
+ "\n",
+ " # Calculate the confidence interval\n",
+ " ci_low, ci_upp = sm.stats.proportion_confint(number_of_successes, n, alpha=alpha, method='binom_test')\n",
+ "\n",
+ " return A_count/(A_count+B_count),ci_low, ci_upp\n",
+ "\n",
+ "def B_count_proportion(df):\n",
+ " A_count = 0\n",
+ " B_count = 0\n",
+ " for index, _ in df.iterrows():\n",
+ " row = df.iloc[index]\n",
+ " if \"Program A\" in row['Answer.category.label']:\n",
+ " A_count += 1\n",
+ " elif \"Program B\" in row['Answer.category.label']:\n",
+ " B_count += 1\n",
+ " number_of_successes = B_count # number of times program A (or B) was chosen as more realistic\n",
+ " n = (A_count+B_count) # total number of submissions\n",
+ "\n",
+ " # Confidence level: 99%\n",
+ " confidence_level = 0.99\n",
+ " alpha = 1 - confidence_level\n",
+ "\n",
+ " # Calculate the confidence interval\n",
+ " ci_low, ci_upp = sm.stats.proportion_confint(number_of_successes, n, alpha=alpha, method='binom_test')\n",
+ "\n",
+ " return B_count/(A_count+B_count),ci_low, ci_upp\n",
+ "\n",
+ "def count_errors(df):\n",
+ " error_count = 0\n",
+ " not_sure_count = 0\n",
+ " for index, _ in df.iterrows():\n",
+ " row = df.iloc[index]\n",
+ " if 'Yes' in row['Answer.category.label']:\n",
+ " error_count += 1\n",
+ " if 'Not Sure' in row['Answer.category.label']:\n",
+ " not_sure_count += 1\n",
+ " return error_count/(df.shape[0]-not_sure_count)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Infinigen first person layout is more realistic than ATISS 0.693 of the time. 99% confidence interval: 0.590 - 0.783\n",
+ "Infinigen first person is more realistic than ATISS 0.713 of the time. 99% confidence interval: 0.611 - 0.802\n",
+ "Infinigen first person layout is more realistic than Sceneformer 0.560 of the time. 99% confidence interval: 0.453 - 0.661\n",
+ "Infinigen first person is more realistic than Sceneformer 0.667 of the time. 99% confidence interval: 0.561 - 0.759\n",
+ "Infinigen first person layout is more realistic than FastSynth 0.853 of the time. 99% confidence interval: 0.766 - 0.917\n",
+ "Infinigen first person is more realistic than FastSynth 0.907 of the time. 99% confidence interval: 0.829 - 0.954\n",
+ "Infinigen first person layout is more realistic than Procthor 0.944 of the time. 99% confidence interval: 0.873 - 0.979\n",
+ "Infinigen first person is more realistic than Procthor 0.893 of the time. 99% confidence interval: 0.813 - 0.946\n",
+ "Infinigen overhead layout is more realistic than ATISS 0.393 of the time. 99% confidence interval: 0.295 - 0.500\n",
+ "Infinigen overhead is more realistic than ATISS 0.480 of the time. 99% confidence interval: 0.376 - 0.586\n",
+ "Infinigen overhead layout is more realistic than Sceneformer 0.573 of the time. 99% confidence interval: 0.466 - 0.675\n",
+ "Infinigen overhead is more realistic than Sceneformer 0.620 of the time. 99% confidence interval: 0.513 - 0.719\n",
+ "Infinigen overhead layout is more realistic than FastSynth 0.560 of the time. 99% confidence interval: 0.453 - 0.661\n",
+ "Infinigen overhead is more realistic than FastSynth 0.453 of the time. 99% confidence interval: 0.350 - 0.561\n"
+ ]
+ }
+ ],
+ "source": [
+ "src = './results/infinigen-ATISS-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person layout is more realistic than ATISS {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-ATISS-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person is more realistic than ATISS {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person layout is more realistic than Sceneformer {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-realism.csv' \n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person is more realistic than Sceneformer {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person layout is more realistic than FastSynth {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person is more realistic than FastSynth {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person layout is more realistic than Procthor {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person is more realistic than Procthor {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-ATISS-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead layout is more realistic than ATISS {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-ATISS-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead is more realistic than ATISS {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead layout is more realistic than Sceneformer {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead is more realistic than Sceneformer {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead layout is more realistic than FastSynth {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen overhead is more realistic than FastSynth {A_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {A_count_proportion(df)[1]:.3f} - {A_count_proportion(df)[2]:.3f}')\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Infinigen first person has 0.175 errors\n"
+ ]
+ }
+ ],
+ "source": [
+ "src = './results/infinigen-first-person-errors.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Infinigen first person has {count_errors(df):.3f} errors')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ATISS first person layout is more realistic than Infinigen 0.307 of the time. 99% confidence interval: 0.217 - 0.410\n",
+ "ATISS first person is more realistic than Infinigen 0.287 of the time. 99% confidence interval: 0.198 - 0.389\n",
+ "Sceneformer first person layout is more realistic than Infinigen 0.440 of the time. 99% confidence interval: 0.339 - 0.547\n",
+ "Sceneformer first person is more realistic than Infinigen 0.333 of the time. 99% confidence interval: 0.241 - 0.439\n",
+ "FastSynth first person layout is more realistic than Infinigen 0.147 of the time. 99% confidence interval: 0.083 - 0.234\n",
+ "Procthor first person layout is more realistic than Infinigen 0.056 of the time. 99% confidence interval: 0.021 - 0.127\n",
+ "Procthor first person is more realistic than Infinigen 0.107 of the time. 99% confidence interval: 0.054 - 0.187\n",
+ "FastSynth first person is more realistic than Infinigen 0.093 of the time. 99% confidence interval: 0.046 - 0.171\n",
+ "ATISS overhead layout is more realistic than Infinigen 0.607 of the time. 99% confidence interval: 0.500 - 0.705\n",
+ "ATISS overhead is more realistic than Infinigen 0.520 of the time. 99% confidence interval: 0.414 - 0.624\n",
+ "sceneformer overhead layout is more realistic than Infinigen 0.427 of the time. 99% confidence interval: 0.325 - 0.534\n",
+ "sceneformer overhead is more realistic than Infinigen 0.380 of the time. 99% confidence interval: 0.281 - 0.487\n",
+ "fastsynth overhead layout is more realistic than Infinigen 0.440 of the time. 99% confidence interval: 0.339 - 0.547\n",
+ "fastsynth overhead is more realistic than Infinigen 0.547 of the time. 99% confidence interval: 0.439 - 0.650\n"
+ ]
+ }
+ ],
+ "source": [
+ "src = './results/infinigen-ATISS-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'ATISS first person layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-ATISS-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'ATISS first person is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Sceneformer first person layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-realism.csv' \n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Sceneformer first person is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'FastSynth first person layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'FastSynth first person is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Procthor first person layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'Procthor first person is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-ATISS-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'ATISS overhead layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-ATISS-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'ATISS overhead is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'sceneformer overhead layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'sceneformer overhead is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-overhead-layout-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'fastsynth overhead layout is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-overhead-realism.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'fastsynth overhead is more realistic than Infinigen {B_count_proportion(df)[0]:.3f} of the time. 99% confidence interval: {B_count_proportion(df)[1]:.3f} - {B_count_proportion(df)[2]:.3f}')\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "procthor first person has 0.252 errors\n"
+ ]
+ }
+ ],
+ "source": [
+ "src = './results/procthor-first-person-errors.csv'\n",
+ "df = pd.read_csv(src)\n",
+ "print(f'procthor first person has {count_errors(df):.3f} errors')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.795, 0.7496838235735617, 0.8348538077362546)"
+ ]
+ },
+ "execution_count": 27,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import statsmodels.api as sm\n",
+ "\n",
+ "def A_count_proportion(dataframes):\n",
+ " A_count = 0\n",
+ " total_B_count = 0\n",
+ " for df in dataframes:\n",
+ " B_count = 0\n",
+ " for index, row in df.iterrows():\n",
+ " if \"Program A\" in row['Answer.category.label']:\n",
+ " A_count += 1\n",
+ " else: # Assuming any other label is a different Program B\n",
+ " B_count += 1\n",
+ " total_B_count += B_count\n",
+ "\n",
+ " number_of_successes = A_count # number of times program A was chosen as more realistic\n",
+ " n = A_count + total_B_count # total number of submissions\n",
+ "\n",
+ " # Confidence level: 99%\n",
+ " confidence_level = 0.99\n",
+ " alpha = 1 - confidence_level\n",
+ "\n",
+ " # Calculate the confidence interval\n",
+ " ci_low, ci_upp = sm.stats.proportion_confint(number_of_successes, n, alpha=alpha, method='binom_test')\n",
+ "\n",
+ " return A_count / n, ci_low, ci_upp\n",
+ "\n",
+ "src = './results/infinigen-ATISS-first-person-realism.csv'\n",
+ "df1 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-realism.csv' \n",
+ "df2 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-realism.csv'\n",
+ "df3 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-realism.csv'\n",
+ "df4 = pd.read_csv(src)\n",
+ "\n",
+ "A_count_proportion([df1,df2,df3,df4])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.7601351351351351, 0.7124366959489067, 0.8029941663408575)"
+ ]
+ },
+ "execution_count": 28,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "src = './results/infinigen-ATISS-first-person-layout-realism.csv'\n",
+ "df1 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-sceneformer-first-person-layout-realism.csv' \n",
+ "df2 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-fastsynth-first-person-layout-realism.csv'\n",
+ "df3 = pd.read_csv(src)\n",
+ "\n",
+ "src = './results/infinigen-procthor-first-person-layout-realism.csv'\n",
+ "df4 = pd.read_csv(src)\n",
+ "\n",
+ "A_count_proportion([df1,df2,df3,df4])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "infinigen_indoors",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/infinigen/tools/perceptual/create_pairs.py b/infinigen/tools/perceptual/create_pairs.py
new file mode 100644
index 000000000..4f86e65ac
--- /dev/null
+++ b/infinigen/tools/perceptual/create_pairs.py
@@ -0,0 +1,187 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import sys
+import cv2
+import os
+import numpy as np
+import matplotlib.pyplot as plt
+
+import sys
+
+
+import pandas as pd
+from PIL import Image
+from tqdm import tqdm
+
+from PIL import Image
+from PIL import Image, ImageDraw, ImageFont
+import random
+
+def merge_images(image_path1, image_path2, text1='Program A', text2='Program B', strip_width=5):
+ # Open the images
+ image1 = Image.open(image_path1)
+ image2 = Image.open(image_path2)
+
+ # Resize the larger image to match the width of the smaller one
+ if image1.width > image2.width:
+ # Calculate new height to maintain aspect ratio
+ new_height = int((image2.width / image1.width) * image1.height)
+ image1 = image1.resize((image2.width, new_height))
+ elif image2.width > image1.width:
+ # Calculate new height to maintain aspect ratio
+ new_height = int((image1.width / image2.width) * image2.height)
+ image2 = image2.resize((image1.width, new_height))
+
+ # Determine the max height
+ max_height = max(image1.height, image2.height)
+
+ # Create a new image with the combined width plus the strip width and the max height
+ combined_width = image1.width + image2.width + strip_width
+ combined_image = Image.new('RGB', (combined_width, max_height), 'black')
+
+ # Paste the two images into the new image
+ # Adjust the position if one image is shorter than the other
+ image1_y = (max_height - image1.height) // 2
+ image2_y = (max_height - image2.height) // 2
+
+ combined_image.paste(image1, (0, image1_y))
+ combined_image.paste(image2, (image1.width + strip_width, image2_y))
+
+ # Add text
+ font_size = 40
+ draw = ImageDraw.Draw(combined_image)
+ try:
+ # Load a specific TrueType or OpenType font file
+ font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial Black.ttf", font_size)
+ except IOError:
+ # If the specific font file is not found, load the default font
+ print('Font not found, using default font.')
+ font = ImageFont.load_default()
+
+ text_color = (255, 0, 0) # White color
+
+ # Calculate text position
+ text1_x = 10
+ text1_y = 10
+ text2_x = image1.width + strip_width + 10
+ text2_y = 10
+
+ draw.text((text1_x, text1_y), text1, fill=text_color, font=font)
+ draw.text((text2_x, text2_y), text2, fill=text_color, font=font)
+
+ # Save the combined image
+ return combined_image
+
+from PIL import Image, ImageDraw, ImageFont
+
+def merge_images2(image_path1, image_path2, text1='Program A', text2='Program B', strip_width=5):
+ # Open the images
+ image1 = Image.open(image_path1)
+ image2 = Image.open(image_path2)
+
+ # Resize the larger image to match the height of the smaller one
+ if image1.height > image2.height:
+ # Calculate new width to maintain aspect ratio
+ new_width = int((image2.height / image1.height) * image1.width)
+ image1 = image1.resize((new_width, image2.height))
+ elif image2.height > image1.height:
+ # Calculate new width to maintain aspect ratio
+ new_width = int((image1.height / image2.height) * image2.width)
+ image2 = image2.resize((new_width, image1.height))
+
+ # Determine the max width after resizing
+ max_width = image1.width + image2.width + strip_width
+
+ # Create a new image with the max width and the combined height
+ combined_image = Image.new('RGB', (max_width, image1.height), 'black')
+
+ # Paste the two images into the new image
+ image1_x = (max_width - image1.width - image2.width - strip_width) // 2
+ image2_x = image1_x + image1.width + strip_width
+
+ combined_image.paste(image1, (image1_x, 0))
+ combined_image.paste(image2, (image2_x, 0))
+
+ # Add text
+ font_size = 40
+ draw = ImageDraw.Draw(combined_image)
+ try:
+ # Load a specific TrueType or OpenType font file
+ font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", font_size)
+ except IOError:
+ # If the specific font file is not found, load the default font
+ print('Font not found, using default font.')
+ font = ImageFont.load_default()
+
+ text_color = (255, 0, 0) # Red color
+
+ # Calculate text position
+ text1_x = 10
+ text1_y = 10
+ text2_x = image1.width + strip_width + 10
+ text2_y = 10
+
+
+ draw.text((text1_x, text1_y), text1, fill=text_color, font=font)
+ draw.text((text2_x, text2_y), text2, fill=text_color, font=font)
+
+ # Save the combined image
+ return combined_image
+
+
+
+if __name__ == '__main__':
+ # methods = ['eevee', 'fastsynth']
+ # perspective = 'first_person'
+ main_directory = sys.argv[1]
+ methods = sys.argv[2]
+ perspective = sys.argv[3]
+ output_directory = sys.argv[4]
+
+ random_seed = 1234 # You can choose any number as the seed
+ random.seed(random_seed)
+
+ k = 50
+ # Set your main directory, methods, and perspective here
+
+
+ if not os.path.exists(output_directory):
+ os.makedirs(output_directory)
+
+ # Building paths for both methods
+ path_method1 = os.path.join(main_directory, methods[0], perspective)
+ path_method2 = os.path.join(main_directory, methods[1], perspective)
+
+ # List of images in each method's perspective directory
+ images_method1 = os.listdir(path_method1)
+ images_method2 = os.listdir(path_method2)
+
+ # Randomly select k images from each list (or all images if there are fewer than k)
+ random_images_method1 = random.sample(images_method1, min(k, len(images_method1)))
+ random_images_method2 = random.sample(images_method2, min(k, len(images_method2)))
+
+ # Iterate over each randomly selected image in method1 and pair it with each randomly selected image in method2
+ for img1 in tqdm(random_images_method1):
+ for img2 in random_images_method2:
+ image_path_1 = os.path.join(path_method1, img1)
+ image_path_2 = os.path.join(path_method2, img2)
+ # Extracting image identifiers
+ img_0_id = img1.split('.')[0]
+ img_1_id = img2.split('.')[0]
+
+ # skip if not image
+ if not (image_path_1.endswith('.png') or image_path_1.endswith('.jpg')):
+ continue
+ if not (image_path_2.endswith('.png') or image_path_2.endswith('.jpg')):
+ continue
+
+ # Creating a unique filename for the merged image
+ merged_filename = f'{perspective}-{methods[0]}-{img_0_id}-{methods[1]}-{img_1_id}.jpg'
+ merged_image_path = os.path.join(output_directory, merged_filename)
+
+ # Merge and save images
+ merged_img = merge_images2(image_path_1, image_path_2)
+ merged_img.save(merged_image_path, 'JPEG')
diff --git a/infinigen/tools/perceptual/create_submission.py b/infinigen/tools/perceptual/create_submission.py
new file mode 100644
index 000000000..b3245051c
--- /dev/null
+++ b/infinigen/tools/perceptual/create_submission.py
@@ -0,0 +1,37 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import os
+import random
+import csv
+import sys
+
+def select_random_files_to_csv(folder_path, k, output_directory):
+ # Get all files in the folder
+ files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
+
+ # Select k random files
+ selected_files = random.sample(files, min(k, len(files)))
+
+ # Create CSV file with the name of the folder
+ folder_name = os.path.basename(folder_path)
+ csv_file_path = os.path.join(output_directory, f'{folder_name}-{k}.csv')
+
+ # Write the selected file names to the CSV
+ with open(csv_file_path, 'w', newline='') as csvfile:
+ writer = csv.writer(csvfile)
+ writer.writerow(['image_url']) # Header
+ for file in selected_files:
+ writer.writerow([file])
+
+ print(f'CSV file created at {csv_file_path}')
+
+
+if __name__ == '__main__':
+ k = 50
+ output_directory = sys.argv[1]
+ folder_path = sys.argv[2]
+ # Example usage
+ select_random_files_to_csv(folder_path, k, output_directory)
diff --git a/infinigen/tools/perceptual/perceptual_extract.py b/infinigen/tools/perceptual/perceptual_extract.py
new file mode 100644
index 000000000..563fe59a1
--- /dev/null
+++ b/infinigen/tools/perceptual/perceptual_extract.py
@@ -0,0 +1,34 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import os
+import shutil
+import sys
+
+
+if __name__ == '__main__':
+ input_directory = sys.argv[1]
+ output_directory = sys.argv[2]
+
+ # Supported image formats
+ image_formats = ['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff']
+
+ if not os.path.exists(output_directory):
+ os.makedirs(output_directory)
+
+ for folder_name in os.listdir(input_directory):
+ folder_path = os.path.join(input_directory, folder_name)
+
+ if os.path.isdir(folder_path):
+ # Find the image file inside the folder
+ for file_name in os.listdir(folder_path):
+ if any(file_name.lower().endswith(ext) for ext in image_formats):
+ old_file_path = os.path.join(folder_path, file_name)
+ new_file_name = f'{folder_name}.png' # Change the extension if needed
+ new_file_path = os.path.join(output_directory, new_file_name)
+
+ # Move and rename the image file
+ shutil.copy(old_file_path, new_file_path)
+ break # Assuming only one image per folder
diff --git a/infinigen/tools/perceptual/rename.py b/infinigen/tools/perceptual/rename.py
new file mode 100644
index 000000000..8f51cd65a
--- /dev/null
+++ b/infinigen/tools/perceptual/rename.py
@@ -0,0 +1,25 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Karhan Kayan
+
+import os
+import sys
+
+
+if __name__ == '__main__':
+ # Set the directory containing your files
+ directory = sys.argv[1]
+
+ # List all files in the directory
+ files = os.listdir(directory)
+
+ # Sort files if necessary
+ files.sort() # This sorts in lexicographical order
+
+ # Rename each file
+ for i, filename in enumerate(files, start=1):
+ old_path = os.path.join(directory, filename)
+ _, file_extension = os.path.splitext(filename)
+ new_path = os.path.join(directory, f'{i}{file_extension}')
+ os.rename(old_path, new_path)
\ No newline at end of file
diff --git a/infinigen/tools/results/visualize_planar_graph.py b/infinigen/tools/results/visualize_planar_graph.py
new file mode 100644
index 000000000..b88480b8b
--- /dev/null
+++ b/infinigen/tools/results/visualize_planar_graph.py
@@ -0,0 +1,71 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+
+import math
+import os
+import sys
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+
+sys.path.insert(0, os.getcwd())
+from PIL import Image
+from infinigen.core.util.math import FixedSeed
+# noinspection PyUnresolvedReferences
+from infinigen.core.util import blender as butil
+from infinigen_examples.generate_individual_assets import make_args
+from infinigen.core.constraints.example_solver.room import GraphMaker
+
+def build_scene(idx, path):
+ with FixedSeed(idx):
+ factory = GraphMaker(idx)
+ graph = factory.make_graph(idx)
+ factory.draw(graph)
+ (path / 'images').mkdir(exist_ok=True)
+ imgpath = path / f"images/image_{idx:03d}.png"
+ plt.savefig(imgpath)
+ plt.clf()
+
+
+def make_grid(args, path, n):
+ files = []
+ for filename in sorted(os.listdir(f'{path}/images')):
+ if filename.endswith('.png'):
+ files.append(f'{path}/images/{filename}')
+ files = files[:n]
+ if len(files) == 0:
+ print('No images found')
+ return
+ with Image.open(files[0]) as i:
+ x, y = i.size
+ sz_x = list(sorted(range(1, n + 1), key=lambda x: abs(math.ceil(n / x) / x - args.best_ratio)))[0]
+ sz_y = math.ceil(n / sz_x)
+ img = Image.new('RGBA', (sz_x * x, sz_y * y))
+ for idx, file in enumerate(files):
+ with Image.open(file) as i:
+ img.paste(i, (idx % sz_x * x, idx // sz_x * y))
+ img.save(f'{path}/grid.png')
+
+
+def main(args):
+ path = Path(os.getcwd()) / 'outputs'
+ path.mkdir(exist_ok=True)
+ fac_path = path / GraphMaker.__name__
+ if fac_path.exists() and args.skip_existing:
+ return
+ fac_path.mkdir(exist_ok=True)
+ n_images = args.n_images
+ for idx in range(args.n_images):
+ fac_path.mkdir(exist_ok=True)
+ build_scene(idx, fac_path)
+
+ make_grid(args, fac_path, n_images)
+
+
+if __name__ == '__main__':
+ args = make_args()
+ args.no_mod = args.no_mod or args.fire
+ with FixedSeed(1):
+ main(args)
diff --git a/infinigen/tools/suffixes.py b/infinigen/tools/suffixes.py
index b2126f8e7..3a8ce7bbe 100644
--- a/infinigen/tools/suffixes.py
+++ b/infinigen/tools/suffixes.py
@@ -43,6 +43,7 @@ def parse_suffix(s):
if len(s_parts) == len(SUFFIX_ORDERING) + 1:
s_parts = s_parts[1:] # discard leading filename / description etc
- assert len(s_parts) == len(SUFFIX_ORDERING), s
+ if len(s_parts) != len(SUFFIX_ORDERING):
+ return None
return {SUFFIX_ORDERING[i]: int(s_parts[i]) for i in range(len(s_parts))}
diff --git a/infinigen/tools/terrain/palette/readme.md b/infinigen/tools/terrain/palette/readme.md
index 1f964d395..fe9e8568d 100644
--- a/infinigen/tools/terrain/palette/readme.md
+++ b/infinigen/tools/terrain/palette/readme.md
@@ -32,7 +32,7 @@ After manually comemnt out them, you have:

-Then you move the ready palatte to location: `infinigen/infinigen_examples/configs/palette`
+Then you move the ready palatte to location: `infinigen/infinigen_examples/configs_nature/palette`
## Step 3
diff --git a/infinigen_examples/asset_parameters.py b/infinigen_examples/asset_parameters.py
new file mode 100644
index 000000000..dd3d9d1ae
--- /dev/null
+++ b/infinigen_examples/asset_parameters.py
@@ -0,0 +1,45 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Lingjie Mei
+import numpy as np
+
+from infinigen.assets.clothes import blanket
+from infinigen.assets.materials import metal, fabrics, ceramic
+from infinigen.assets.materials.woods import wood
+from infinigen.assets.scatters.clothes import ClothesCover
+from infinigen.assets.seating import ChairFactory
+from infinigen.assets.tableware import PotFactory, PanFactory, FruitContainerFactory
+from infinigen.core.surface import NoApply
+
+parameters = {
+ 'ChairFactory': {
+ 'factories': [ChairFactory] * 16,
+ 'globals': {
+ },
+ 'individuals': [{}, {'arm_mid': [-.03, -.03, .09], 'leg_height': .5, 'leg_x_offset': 0},
+ {'arm_mid': [0, 0, 0], 'leg_height': .6, 'leg_x_offset': .02},
+ {'arm_mid': [.03, .09, -.03], 'leg_height': .7, 'leg_x_offset': .05}, {},
+ {'leg_offset_bar': (.2, .4), 'seat_front': 1., 'back_vertical_cuts': 1},
+ {'leg_offset_bar': (.4, .6), 'seat_front': 1.1, 'back_vertical_cuts': 2},
+ {'leg_offset_bar': (.6, .8), 'seat_front': 1.2, 'back_vertical_cuts': 3}, {}] + [{}] * 7,
+ 'repeat': 12,
+ 'indices': [0] * 9 + list(range(1, 8)),
+ 'scene_idx': 4,
+ },
+
+ 'PanFactory': {
+ 'factories': [PanFactory] * 8 + [PanFactory] * 2 + [PotFactory] * 3 + [FruitContainerFactory] * 3,
+ 'globals': {
+ },
+ 'individuals': [{}, {'scale': .1, 'depth': .3, 'x_handle': 2, }, {'scale': .12, 'depth': .5, 'x_handle': 1.5},
+ {'scale': .15, 'depth': .8, 'x_handle': 1.2}, {},
+ {'s_handle': .8, 'r_expand': 1, 'x_guard': 1, },
+ {'s_handle': 1., 'r_expand': 1.15, 'x_guard': 1.3},
+ {'s_handle': 1.2, 'r_expand': 1.3, 'x_guard': 1.6}, {}] + [{}] * 7,
+ 'repeat': 12,
+ 'indices': [0] * 9 + list(range(1, 8)),
+ 'scene_idx': 2,
+ },
+
+}
diff --git a/infinigen_examples/configs/disable_assets/no_assets.gin b/infinigen_examples/configs/disable_assets/no_assets.gin
deleted file mode 100644
index 5c8297d39..000000000
--- a/infinigen_examples/configs/disable_assets/no_assets.gin
+++ /dev/null
@@ -1,47 +0,0 @@
-compose_scene.fancy_clouds_chance = 0.0
-
-compose_scene.trees_chance = 0.0
-compose_scene.bushes_chance = 0.0
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.flying_creatures_chance = 0.0
-compose_scene.clouds_chance = 0.0
-compose_scene.boulders_chance = 0.0
-compose_scene.glowing_rocks_chance = 0.0
-compose_scene.rocks_chance = 0.0
-compose_scene.ground_leaves_chance = 0.0
-compose_scene.ground_twigs_chance = 0.0
-compose_scene.chopped_trees_chance = 0.0
-compose_scene.grass_chance = 0.0
-compose_scene.flowers_chance = 0.0
-compose_scene.kelp_chance = 0.0
-compose_scene.cactus_chance = 0.0
-compose_scene.rain_particles_chance = 0.0
-compose_scene.snow_particles_chance = 0.0
-compose_scene.leaf_particles_chance = 0.0
-compose_scene.dust_particles_chance = 0.0
-compose_scene.camera_based_lighting_chance = 0.0
-compose_scene.wind_chance = 0.0
-compose_scene.turbulence_chance = 0.0
-compose_scene.ferns_chance = 0.0
-
-compose_scene.fish_school_chance = 0.0
-compose_scene.marine_snow_particles_chance = 0.0
-
-compose_scene.corals_chance = 0.0
-compose_scene.seaweed_chance = 0.0
-compose_scene.urchin_chance = 0.0
-compose_scene.seashells_chance = 0.0
-compose_scene.jellyfish_chance = 0.0
-compose_scene.mushroom_chance = 0.0
-compose_scene.pinecone_chance = 0.0
-compose_scene.monocots_chance = 0.0
-compose_scene.pine_needle_chance = 0.0
-compose_scene.caustics_chance = 0.0
-
-compose_scene.decorative_plants_chance = 0.0
-
-populate_scene.moss_chance = 0.0
-populate_scene.lichen_chance = 0.0
-populate_scene.slime_mold_chance = 0.0
-populate_scene.ivy_chance = 0.0
-populate_scene.snow_layer_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs/disable_assets/no_creatures.gin b/infinigen_examples/configs/disable_assets/no_creatures.gin
deleted file mode 100644
index 2a751337f..000000000
--- a/infinigen_examples/configs/disable_assets/no_creatures.gin
+++ /dev/null
@@ -1,4 +0,0 @@
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.flying_creatures_chance = 0.0
-compose_scene.fish_school_chance = 0.0
-compose_scene.bug_swarm_chance = 0.0
diff --git a/infinigen_examples/configs/disable_assets/no_particles.gin b/infinigen_examples/configs/disable_assets/no_particles.gin
deleted file mode 100644
index ed26effe0..000000000
--- a/infinigen_examples/configs/disable_assets/no_particles.gin
+++ /dev/null
@@ -1,5 +0,0 @@
-compose_scene.rain_particles_chance = 0.0
-compose_scene.snow_particles_chance = 0.0
-compose_scene.leaf_particles_chance = 0.0
-compose_scene.dust_particles_chance = 0.0
-compose_scene.marine_snow_particles_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs/disable_assets/no_rocks.gin b/infinigen_examples/configs/disable_assets/no_rocks.gin
deleted file mode 100644
index 3ecb60b8e..000000000
--- a/infinigen_examples/configs/disable_assets/no_rocks.gin
+++ /dev/null
@@ -1,2 +0,0 @@
-compose_scene.boulders_chance = 0.0
-compose_scene.rocks_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs/performance/simple.gin b/infinigen_examples/configs/performance/simple.gin
deleted file mode 100644
index 9d3c44c86..000000000
--- a/infinigen_examples/configs/performance/simple.gin
+++ /dev/null
@@ -1,5 +0,0 @@
-include 'performance/dev.gin'
-include 'disable_assets/no_creatures.gin'
-include 'performance/fast_terrain_assets.gin'
-run_erosion.n_iters = [1,1]
-full/configure_render_cycles.num_samples = 100
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/cave.gin b/infinigen_examples/configs/scene_types/cave.gin
deleted file mode 100644
index 739d942a9..000000000
--- a/infinigen_examples/configs/scene_types/cave.gin
+++ /dev/null
@@ -1,60 +0,0 @@
-compose_scene.land_domain_tags = 'landscape,-liquid_covered'
-compose_scene.nonliving_domain_tags = 'landscape'
-compose_scene.underwater_domain_tags = 'landscape,liquid_covered'
-
-compose_scene.trees_chance = 0.4
-compose_scene.rocks_chance = 0.8
-compose_scene.glowing_rocks_chance = 1
-compose_scene.grass_chance = 0.4
-compose_scene.ferns_chance = 0.6
-compose_scene.mushroom_chance = 0.8
-
-compose_scene.snow_particles_chance=0.0
-compose_scene.leaves_chance=0.0
-compose_scene.rain_particles_chance=0.0
-
-surface.registry.liquid_collection = [
- ('water', 0.7),
- ('lava', 0.3)
-]
-
-compose_scene.bug_swarm_chance=0.0
-
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.ground_creature_registry = [
- (@CarnivoreFactory, 1),
- (@HerbivoreFactory, 0.3),
- (@BirdFactory, 0.5),
- (@BeetleFactory, 1)
-]
-
-compose_scene.flying_creatures_chance=0.2
-compose_scene.flying_creature_registry = [
- (@DragonflyFactory, 1),
-]
-
-atmosphere_light_haze.shader_atmosphere.enable_scatter = False
-configure_render_cycles.exposure = 1.3
-
-
-# scene composition config
-scene.caves_chance = 1
-scene.ground_chance = 0.5
-Caves.randomness = 0
-Caves.height_offset = -4
-Caves.frequency = 0.01
-Caves.noise_scale = ("uniform", 2, 5)
-Caves.n_lattice = 1
-Caves.is_horizontal = 1
-Caves.scale_increase = 1
-
-scene.waterbody_chance = 0.8
-Waterbody.height = -5
-
-# camera selection config
-Terrain.populated_bounds = (-25, 25, -25, 25, -25, 0)
-keep_cam_pose_proposal.terrain_coverage_range = (1, 1)
-camera_selection_ranges_ratio.closeup = ("closeup", 4, 0, 0.3)
-camera_selection_tags_ratio.cave = (0.3, 1)
-
-SphericalMesher.r_min = 0.2
diff --git a/infinigen_examples/configs/scene_types/coral_reef.gin b/infinigen_examples/configs/scene_types/coral_reef.gin
deleted file mode 100644
index 86f02b746..000000000
--- a/infinigen_examples/configs/scene_types/coral_reef.gin
+++ /dev/null
@@ -1,11 +0,0 @@
-include 'scene_types/under_water.gin'
-
-compose_scene.kelp_chance = 0.1
-compose_scene.urchin_chance = 0.1
-
-compose_scene.corals_chance = 1.
-compose_scene.seaweed_chance = 0.2
-compose_scene.seashell_chance = 0.7
-compose_scene.jellyfish_chance = 0.0
-
-water.geo.with_waves=True
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/desert.gin b/infinigen_examples/configs/scene_types/desert.gin
deleted file mode 100644
index f4cf0cf1c..000000000
--- a/infinigen_examples/configs/scene_types/desert.gin
+++ /dev/null
@@ -1,46 +0,0 @@
-
-compose_scene.trees_chance = 0.25
-compose_scene.cactus_chance = .7
-compose_scene.ground_leaves_chance = 0.5
-compose_scene.ground_twigs_chance = 0.3
-compose_scene.chopped_trees_chance = 0.1
-compose_scene.grass_chance = 0.1
-compose_scene.ferns_chance = 0.0
-compose_scene.pine_needle_chance = 0.05
-compose_scene.fancy_clouds_chance = 0.0
-
-compose_scene.tree_density = 0.02
-
-compose_scene.rain_particles_chance = 0.0
-compose_scene.snow_particles_chance = 0
-compose_scene.leaf_particles_chance = 0.05
-compose_scene.dust_particles_chance = 0.0
-
-atmosphere_light_haze.shader_atmosphere.density = ("uniform", 0, 0.0015)
-atmosphere_light_haze.shader_atmosphere.anisotropy = 0
-
-animate_cameras.follow_poi_chance=0.5
-
-compose_scene.ground_creatures_chance = 1.0
-compose_scene.ground_creature_registry = [
- (@SnakeFactory, 1)
-]
-
-surface.registry.ground_collection = [
- ('sand', 1),
-]
-
-surface.registry.mountain_collection = [
- ("sandstone", 1),
-]
-
-# scene composition config
-LandTiles.tiles = ["Mountain"]
-LandTiles.randomness = 1
-LandTiles.tile_heights = [-2]
-LandTiles.tile_density = 0.25
-WarpedRocks.slope_shift = -3
-Ground.with_sand_dunes = 1
-
-
-scene.waterbody_chance = 0
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/forest.gin b/infinigen_examples/configs/scene_types/forest.gin
deleted file mode 100644
index ef6d37cc7..000000000
--- a/infinigen_examples/configs/scene_types/forest.gin
+++ /dev/null
@@ -1,58 +0,0 @@
-Ground.scale = 10
-multi_mountains_params.height = 15
-multi_mountains_params.min_freq = ("uniform", 0.008, 0.012)
-multi_mountains_params.max_freq = ("uniform", 0.024, 0.036)
-scene.warped_rocks_chance = 0
-
-compose_scene.inview_distance = 40
-placement.populate_all.dist_cull = 40
-
-surface.registry.ground_collection = [
- ('mud', 2),
- ('dirt', 1),
- ('soil', 1),
-]
-
-surface.registry.mountain_collection = [
- ('dirt', 1),
- ('soil', 1),
- ('cracked_ground', 0.5)
-]
-
-compose_scene.ground_creatures_chance = 0.1
-compose_scene.ground_creature_registry = [
- #(@CarnivoreFactory, 2),
- #(@HerbivoreFactory, 0.8),
- (@SnakeFactory, 2)
-]
-
-compose_scene.flying_creatures_chance = 0.7
-compose_scene.flying_creature_registry = [
- (@DragonflyFactory, 1),
- (@FlyingBirdFactory, 0.2)
-]
-
-compose_scene.bug_swarm_chance = 0.0
-
-compose_scene.trees_chance = 1.0
-compose_scene.tree_density = 0.11
-
-compose_scene.ground_leaves_chance = 0.7
-compose_scene.ground_twigs_chance = 0.7
-compose_scene.chopped_trees_chance = 0.3
-compose_scene.grass_chance = 0.4
-compose_scene.ferns_chance = 0.6
-compose_scene.flowers_chance = 0.4
-compose_scene.monocots_chance = 0.5
-compose_scene.mushroom_chance = 0.3
-compose_scene.pinecone_chance = 0.5
-compose_scene.pine_needle_chance = 0.6
-
-populate_scene.slime_mold_chance = 0.0
-populate_scene.ivy_chance = 0.0
-populate_scene.lichen_chance = 0.6
-populate_scene.mushroom_chance = 0.0
-populate_scene.moss_chance = 0.0
-
-compose_scene.rain_particles_chance = 0.0
-compose_scene.leaf_particles_chance = 0.7
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/kelp_forest.gin b/infinigen_examples/configs/scene_types/kelp_forest.gin
deleted file mode 100644
index e054dad15..000000000
--- a/infinigen_examples/configs/scene_types/kelp_forest.gin
+++ /dev/null
@@ -1,24 +0,0 @@
-include 'scene_types/under_water.gin'
-
-multi_mountains_params.height = ("uniform", 1, 4)
-multi_mountains_params.min_freq = ("uniform", 0.01, 0.015)
-multi_mountains_params.max_freq = ("uniform", 0.03, 0.06)
-
-compose_scene.glowing_rocks_chance = 0.
-compose_scene.ground_leaves_chance = 0.2
-compose_scene.ground_twigs_chance = 0.4
-compose_scene.chopped_trees_chance = 0.
-
-compose_scene.kelp_chance = 1.0
-compose_scene.urchin_chance = 0.7
-
-compose_scene.corals_chance = 0.0
-compose_scene.seaweed_chance = 0.8
-compose_scene.seashell_chance = 0.8
-compose_scene.jellyfish_chance = 0.0
-
-water.shader.volume_density = ("uniform", 0.09, 0.13)
-water.shader.anisotropy = ("uniform", 0.45, 0.7)
-water.geo.with_waves=False
-
-camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 15, 60, 140)
diff --git a/infinigen_examples/configs/scene_types/plain.gin b/infinigen_examples/configs/scene_types/plain.gin
deleted file mode 100644
index a3bbe0a66..000000000
--- a/infinigen_examples/configs/scene_types/plain.gin
+++ /dev/null
@@ -1,28 +0,0 @@
-compose_scene.ground_creature_registry = [
- (@CarnivoreFactory, 1),
- (@HerbivoreFactory, 1.5),
- (@BirdFactory, 0.7),
-]
-
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.ground_creature_registry = [
- (@CarnivoreFactory, 2),
- (@HerbivoreFactory, 0.8),
- (@SnakeFactory, 2)
-]
-
-compose_scene.flying_creatures_chance = 0.7
-compose_scene.flying_creature_registry = [
- (@DragonflyFactory, 1),
- (@FlyingBirdFactory, 0.1)
-]
-
-compose_scene.grass_select_max = 0.35
-compose_scene.tree_density = 0.02
-compose_scene.grass_chance = 1
-
-compose_scene.bug_swarm_chance=0.0
-
-# scene composition config
-scene.landtiles_chance = 0
-scene.warped_rocks_chance = 0
diff --git a/infinigen_examples/configs/scene_types/under_water.gin b/infinigen_examples/configs/scene_types/under_water.gin
deleted file mode 100644
index 0ec73b52a..000000000
--- a/infinigen_examples/configs/scene_types/under_water.gin
+++ /dev/null
@@ -1,81 +0,0 @@
-multi_mountains_params.height = ("uniform", 4, 10)
-multi_mountains_params.min_freq = ("uniform", 0.01, 0.015)
-multi_mountains_params.max_freq = ("uniform", 0.03, 0.06)
-
-keep_cam_pose_proposal.terrain_coverage_range = (0.4, 1)
-camera.camera_pose_proposal.altitude = ("clip_gaussian", 3.5, 0.7, 2, 6)
-camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 15, 60, 110)
-
-compose_scene.trees_chance = 0.
-compose_scene.bushes_chance = 0.
-compose_scene.creatures_chance = 1.0
-compose_scene.glowing_rocks_chance = 0.05
-compose_scene.rocks_chance = 0.5
-compose_scene.ground_leaves_chance = 0.05
-compose_scene.ground_twigs_chance = 0.05
-compose_scene.chopped_trees_chance = 0.1
-compose_scene.grass_chance = 0.05
-compose_scene.kelp_chance = 0.7
-compose_scene.cactus_chance = 0.
-compose_scene.ferns_chance = 0.01
-compose_scene.camera_based_lighting_chance = 1.0
-
-compose_scene.corals_chance = 0.8
-compose_scene.seaweed_chance = 0.8
-compose_scene.seashell_chance = 0.8
-compose_scene.urchin_chance = 0.8
-compose_scene.jellyfish_chance = 0.0
-compose_scene.mushroom_chance = 0.0
-compose_scene.pinecone_chance = 0.0
-compose_scene.pine_needle_chance = 0.0
-compose_scene.boulders_chance = 0.0
-compose_scene.monocots_chance = 0.0
-
-water.geo.water_detail = ("uniform", 0.2, 0.5)
-water.geo.water_height = ("uniform", 0.05, 0.15)
-water.geo.with_ripples = 0
-water.shader.volume_density = ("uniform", 0.03, 0.05)
-water.shader.color = ("color_category", 'under_water')
-water.shader.colored = 0
-water.shader.emissive_foam = 0
-water.is_ocean = False
-
-
-compose_scene.wind_chance = 0
-compose_scene.rain_particles_chance = 0
-compose_scene.snow_particles_chance = 0
-compose_scene.leaf_particles_chance = 0
-compose_scene.dust_particles_chance = 0
-compose_scene.marine_snow_particles_chance = 0
-compose_scene.caustics_chance = 0.6
-
-atmosphere_light_haze.shader_atmosphere.enable_scatter = False
-compose_scene.fancy_clouds_chance=0.0
-
-surface.registry.rock_collection = [
- ('stone', 1),
- ('mountain', 0.5),
-]
-
-compose_scene.ground_creatures_chance = 0.4
-compose_scene.ground_creature_registry = [
- (@CrustaceanFactory, 1),
-]
-compose_scene.flying_creatures_chance = 0.0
-compose_scene.bug_swarm_chance = 0.0
-compose_scene.fish_school_chance = 0.3
-
-# scene composition config
-LandTiles.tile_heights = [-20]
-Ground.height = -20
-Atmosphere.hacky_offset = 0.1
-WarpedRocks.slope_shift = -23
-scene.waterbody_chance = 1
-
-Terrain.under_water = 1
-
-compose_scene.turbulence_chance = 0.7
-turbulence_effector.strength = ("uniform", 0, 7)
-turbulence_effector.size = ("uniform", 1.5, 4.5)
-turbulence_effector.flow = 1
-turbulence_effector.noise = 10
\ No newline at end of file
diff --git a/infinigen_examples/configs/trailer_video.gin b/infinigen_examples/configs/trailer_video.gin
deleted file mode 100644
index 86f8a555f..000000000
--- a/infinigen_examples/configs/trailer_video.gin
+++ /dev/null
@@ -1,8 +0,0 @@
-export.spherical = False # use OcMesher
-AnimPolicyRandomWalkLookaround.speed=('uniform', 1, 2.5),
-AnimPolicyRandomWalkLookaround.step_speed_mult=('uniform', 0.5, 2),
-AnimPolicyRandomWalkLookaround.yaw_sampler=('uniform',-20, 20),
-AnimPolicyRandomWalkLookaround.step_range=('clip_gaussian', 3, 5, 0.5, 10),
-AnimPolicyRandomWalkLookaround.rot_vars=(5, 0, 5),
-AnimPolicyRandomWalkLookaround.motion_dir_zoff=('clip_gaussian', 0, 90, 0, 180)
-execute_tasks.fps = 16
\ No newline at end of file
diff --git a/infinigen_examples/configs_indoor/base.gin b/infinigen_examples/configs_indoor/base.gin
new file mode 100644
index 000000000..bfb6cff9b
--- /dev/null
+++ b/infinigen_examples/configs_indoor/base.gin
@@ -0,0 +1,65 @@
+include 'infinigen_examples/configs_nature/base.gin'
+include 'infinigen_examples/configs_nature/base_surface_registry.gin'
+include 'infinigen_examples/configs_nature/natural.gin'
+include 'infinigen_examples/configs_nature/performance/fast_terrain_assets.gin'
+
+# overriden in fast_solve.gin if present
+compose_indoors.solve_steps_large = 500
+compose_indoors.solve_steps_medium = 200
+compose_indoors.solve_steps_small = 300
+
+SimulatedAnnealingSolver.initial_temp = 3
+SimulatedAnnealingSolver.final_temp = 0.001
+SimulatedAnnealingSolver.finetune_pct = 0.15
+SimulatedAnnealingSolver.max_invalid_candidates = 5
+
+render_image.use_dof = True
+animate_cameras.follow_poi_chance=0.0
+camera.camera_pose_proposal.altitude = ("clip_gaussian", 1.5, 0.8, 0.5, 2.2)
+camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 15, 60, 95)
+camera.camera_pose_proposal.focal_length = 15
+export.spherical = False # spherical mesher doesnt support short focal length / wide fov
+
+camera.spawn_camera_rigs.n_camera_rigs = 1
+camera.spawn_camera_rigs.camera_rig_config = [
+ {'loc': (0, 0, 0), 'rot_euler': (0, 0, 0)},
+ {'loc': (0.075, 0, 0), 'rot_euler': (0, 0, 0)}
+]
+walk_same_altitude.z_move_up = 0
+keep_cam_pose_proposal.min_terrain_distance = 0.7 # stuff will inevitably be closer to the camera indoors
+
+# animating the camera takes more search time when indoors
+animate_trajectory.max_step_tries=40
+animate_trajectory.max_full_retries=20
+compute_base_views.min_candidates_ratio=10
+compute_base_views.max_tries=50000
+compose_indoors.animate_cameras_enabled = False # not yet working robustly
+
+group_collections.config = [
+ {'name': 'assets', 'hide_viewport': True, 'hide_render': True}, # collections of assets used by scatters
+ {'name': 'placeholders', 'hide_viewport': True, 'hide_render': True}, # low-res markers / proxies for where assets will be spawned
+ {'name': 'unique_assets', 'hide_viewport': False, 'hide_render': False}, # actual hi-res assets spawned at each placeholder location
+]
+
+configure_render_cycles.exposure = 3
+configure_render_cycles.denoise = False
+configure_render_cycles.adaptive_threshold = 0.005
+
+nishita_lighting.strength = 0.25
+nishita_lighting.sun_elevation = ("clip_gaussian", 40, 25, 6, 70)
+
+compose_indoors.lights_off_chance=0.2
+compose_indoors.skirting_floor_chance=0.7
+compose_indoors.skirting_ceiling_chance=0.2
+
+compose_indoors.near_distance = 60
+
+compose_indoors.invisible_room_ceilings_enabled = False
+compose_indoors.overhead_cam_enabled = False
+compose_indoors.hide_other_rooms_enabled = False
+
+# for create_outdoor_backdrop
+compose_indoors.fancy_clouds_chance = 0.5
+compose_indoors.grass_chance = 0.5
+compose_indoors.rocks_chance = 0.5
+compose_indoors.near_distance = 20
\ No newline at end of file
diff --git a/infinigen_examples/configs/base_surface_registry.gin b/infinigen_examples/configs_indoor/base_surface_registry.gin
similarity index 100%
rename from infinigen_examples/configs/base_surface_registry.gin
rename to infinigen_examples/configs_indoor/base_surface_registry.gin
diff --git a/infinigen_examples/configs_indoor/disable/no_details.gin b/infinigen_examples/configs_indoor/disable/no_details.gin
new file mode 100644
index 000000000..4de7d683e
--- /dev/null
+++ b/infinigen_examples/configs_indoor/disable/no_details.gin
@@ -0,0 +1,9 @@
+compose_indoors.room_doors_enabled = False
+compose_indoors.room_windows_enabled = False
+
+compose_indoors.room_floors_enabled = False
+compose_indoors.room_walls_enabled = False
+compose_indoors.room_ceilings_enabled = False
+
+compose_indoors.skirting_floor_enabled = False
+compose_indoors.skirting_ceiling_enabled = False
\ No newline at end of file
diff --git a/infinigen_examples/configs_indoor/disable/no_objects.gin b/infinigen_examples/configs_indoor/disable/no_objects.gin
new file mode 100644
index 000000000..0dfeea0e6
--- /dev/null
+++ b/infinigen_examples/configs_indoor/disable/no_objects.gin
@@ -0,0 +1,3 @@
+compose_indoors.solve_large_enabled = False
+compose_indoors.solve_medium_enabled = False
+compose_indoors.solve_small_enabled = False
\ No newline at end of file
diff --git a/infinigen_examples/configs_indoor/export_upload.gin b/infinigen_examples/configs_indoor/export_upload.gin
new file mode 100644
index 000000000..f52381b88
--- /dev/null
+++ b/infinigen_examples/configs_indoor/export_upload.gin
@@ -0,0 +1,2 @@
+export_curr_scene.format = 'obj'
+export_curr_scene.individual_export = True
diff --git a/infinigen_examples/configs_indoor/fast_solve.gin b/infinigen_examples/configs_indoor/fast_solve.gin
new file mode 100644
index 000000000..db561577c
--- /dev/null
+++ b/infinigen_examples/configs_indoor/fast_solve.gin
@@ -0,0 +1,10 @@
+RoomSolver.n_divide_trials = 60
+MultistoryRoomSolver.n_divide_trials = 60
+RoomSolver.iters_mult = 120
+MultistoryRoomSolver.iters_mult = 120
+
+compose_indoors.solve_steps_large = 150
+compose_indoors.solve_steps_medium = 40
+compose_indoors.solve_steps_small = 10
+
+compose_indoors.terrain_enabled = False
diff --git a/infinigen_examples/configs_indoor/multistory.gin b/infinigen_examples/configs_indoor/multistory.gin
new file mode 100644
index 000000000..d1735a868
--- /dev/null
+++ b/infinigen_examples/configs_indoor/multistory.gin
@@ -0,0 +1,14 @@
+execute_tasks.generate_resolution = (720, 720)
+get_sensor_coords.H = 720
+get_sensor_coords.W = 720
+MultistoryRoomSolver.n_divide_trials = 100
+
+compose_indoors.terrain_enabled=False
+compose_indoors.solve_large_enabled=False
+compose_indoors.solve_small_enabled=False
+compose_indoors.solve_medium_enabled=False
+
+compose_indoors.topview=True
+compose_indoors.topview_rot_x=45
+compose_indoors.topview_rot_z=-45
+Solver.multistory=True
diff --git a/infinigen_examples/configs/natural.gin b/infinigen_examples/configs_indoor/natural.gin
similarity index 100%
rename from infinigen_examples/configs/natural.gin
rename to infinigen_examples/configs_indoor/natural.gin
diff --git a/infinigen_examples/configs_indoor/overhead.gin b/infinigen_examples/configs_indoor/overhead.gin
new file mode 100644
index 000000000..caa0b913c
--- /dev/null
+++ b/infinigen_examples/configs_indoor/overhead.gin
@@ -0,0 +1,15 @@
+compose_indoors.invisible_room_ceilings_enabled = True
+compose_indoors.hide_other_rooms_enabled = True
+
+compose_indoors.pose_cameras_enabled = False
+compose_indoors.overhead_cam_enabled = True
+
+compose_indoors.terrain_enabled = False
+compose_indoors.nature_backdrop_enabled = False
+
+compose_indoors.lights_off_chance = 0.0
+
+compose_indoors.skirting_floor_chance = 0.0
+compose_indoors.skirting_ceiling_chance = 0.0
+
+
diff --git a/infinigen_examples/configs_indoor/singleroom.gin b/infinigen_examples/configs_indoor/singleroom.gin
new file mode 100644
index 000000000..3425975e8
--- /dev/null
+++ b/infinigen_examples/configs_indoor/singleroom.gin
@@ -0,0 +1,2 @@
+BlueprintSolidifier.enable_open=False
+restrict_solving.solve_max_rooms=1
\ No newline at end of file
diff --git a/infinigen_examples/configs_indoor/studio.gin b/infinigen_examples/configs_indoor/studio.gin
new file mode 100644
index 000000000..fcd21565a
--- /dev/null
+++ b/infinigen_examples/configs_indoor/studio.gin
@@ -0,0 +1 @@
+GraphMaker.room_children='studio'
\ No newline at end of file
diff --git a/infinigen_examples/configs_indoor/topview.gin b/infinigen_examples/configs_indoor/topview.gin
new file mode 100644
index 000000000..b91616af8
--- /dev/null
+++ b/infinigen_examples/configs_indoor/topview.gin
@@ -0,0 +1,8 @@
+execute_tasks.generate_resolution = (720, 720)
+get_sensor_coords.H = 720
+get_sensor_coords.W = 720
+
+compose_indoors.topview=True
+compose_indoors.terrain_enabled=False
+compose_indoors.solve_large_enabled=False
+compose_indoors.solve_small_enabled=False
diff --git a/infinigen_examples/configs/.gitignore b/infinigen_examples/configs_nature/.gitignore
similarity index 100%
rename from infinigen_examples/configs/.gitignore
rename to infinigen_examples/configs_nature/.gitignore
diff --git a/infinigen_examples/configs/asset_demo.gin b/infinigen_examples/configs_nature/asset_demo.gin
similarity index 71%
rename from infinigen_examples/configs/asset_demo.gin
rename to infinigen_examples/configs_nature/asset_demo.gin
index 87ac99931..c19c8ea99 100644
--- a/infinigen_examples/configs/asset_demo.gin
+++ b/infinigen_examples/configs_nature/asset_demo.gin
@@ -1,9 +1,9 @@
-compose_scene.inview_distance = 30
+compose_nature.inview_distance = 30
full/configure_render_cycles.min_samples = 50
full/configure_render_cycles.num_samples = 300
configure_blender.motion_blur = False
-configure_blender.use_dof = True
+render_image.use_dof = True
full/render_image.passes_to_save = []
\ No newline at end of file
diff --git a/infinigen_examples/configs/base.gin b/infinigen_examples/configs_nature/base.gin
similarity index 67%
rename from infinigen_examples/configs/base.gin
rename to infinigen_examples/configs_nature/base.gin
index 5a0af02c9..b10263a26 100644
--- a/infinigen_examples/configs/base.gin
+++ b/infinigen_examples/configs_nature/base.gin
@@ -14,83 +14,83 @@ save_obj_and_instances.output_folder="saved_mesh.obj"
util.logging.create_text_file.log_dir = %LOG_DIR
placement.populate_all.dist_cull = 70
-compose_scene.inview_distance = 70
-compose_scene.near_distance = 20
-compose_scene.center_distance = 35
+compose_nature.inview_distance = 70
+compose_nature.near_distance = 20
+compose_nature.center_distance = 35
-compose_scene.land_domain_tags = 'landscape,-liquid_covered,-cave,-beach'
-compose_scene.nonliving_domain_tags = 'landscape,-cave'
-compose_scene.underwater_domain_tags = 'landscape,liquid_covered,-cave'
+compose_nature.land_domain_tags = 'landscape,-liquid_covered,-cave,-beach'
+compose_nature.nonliving_domain_tags = 'landscape,-cave'
+compose_nature.underwater_domain_tags = 'landscape,liquid_covered,-cave'
-compose_scene.terrain_enabled = True
-compose_scene.lighting_enabled = True
-compose_scene.coarse_terrain_enabled = True
-compose_scene.terrain_surface_enabled = True
+compose_nature.terrain_enabled = True
+compose_nature.lighting_enabled = True
+compose_nature.coarse_terrain_enabled = True
+compose_nature.terrain_surface_enabled = True
-compose_scene.simulated_river_enabled=False
-compose_scene.tilted_river_enabled=False
+compose_nature.simulated_river_enabled=False
+compose_nature.tilted_river_enabled=False
-compose_scene.fancy_clouds_chance = 0.6
+compose_nature.fancy_clouds_chance = 0.6
-compose_scene.trees_chance = 0.85
-compose_scene.bushes_chance = 0.7
-compose_scene.clouds_chance = 0.0
-compose_scene.boulders_chance = 0.7
+compose_nature.trees_chance = 0.85
+compose_nature.bushes_chance = 0.7
+compose_nature.clouds_chance = 0.0
+compose_nature.boulders_chance = 0.7
-compose_scene.glowing_rocks_chance = 0.0
-compose_scene.rocks_chance = 0.9
+compose_nature.glowing_rocks_chance = 0.0
+compose_nature.rocks_chance = 0.9
-compose_scene.ground_leaves_chance = 0.7
-compose_scene.ground_twigs_chance = 0.7
-compose_scene.chopped_trees_chance = 0.7
+compose_nature.ground_leaves_chance = 0.7
+compose_nature.ground_twigs_chance = 0.7
+compose_nature.chopped_trees_chance = 0.7
-compose_scene.grass_chance = 0.8
-compose_scene.ferns_chance = 0.25
-compose_scene.monocots_chance = 0.15
+compose_nature.grass_chance = 0.8
+compose_nature.ferns_chance = 0.25
+compose_nature.monocots_chance = 0.15
-compose_scene.flowers_chance = 0.2
-compose_scene.kelp_chance = 0.0
-compose_scene.cactus_chance = 0.0
-compose_scene.coconut_trees_chance = 0.0
-compose_scene.palm_trees_chance = 0.0
+compose_nature.flowers_chance = 0.2
+compose_nature.kelp_chance = 0.0
+compose_nature.cactus_chance = 0.0
+compose_nature.coconut_trees_chance = 0.0
+compose_nature.palm_trees_chance = 0.0
-compose_scene.instanced_trees_chance = 0.0 # conditioned on trees_chance as prereq
+compose_nature.instanced_trees_chance = 0.0 # conditioned on trees_chance as prereq
-compose_scene.fish_school_chance = 0.0
-compose_scene.bug_swarm_chance = 0.0
+compose_nature.fish_school_chance = 0.0
+compose_nature.bug_swarm_chance = 0.0
-compose_scene.rain_particles_chance = 0.0
-compose_scene.snow_particles_chance = 0.0
-compose_scene.leaf_particles_chance = 0.0
-compose_scene.dust_particles_chance = 0.0
-compose_scene.marine_snow_particles_chance = 0.0
-compose_scene.camera_based_lighting_chance = 0.0
+compose_nature.rain_particles_chance = 0.0
+compose_nature.snow_particles_chance = 0.0
+compose_nature.leaf_particles_chance = 0.0
+compose_nature.dust_particles_chance = 0.0
+compose_nature.marine_snow_particles_chance = 0.0
+compose_nature.camera_based_lighting_chance = 0.0
-compose_scene.wind_chance = 0.5
-compose_scene.turbulence_chance = 0.3
+compose_nature.wind_chance = 0.5
+compose_nature.turbulence_chance = 0.3
wind_effector.strength = ('uniform', 0, 0.02)
turbulence_effector.strength = ('uniform', 0, 0.02)
turbulence_effector.noise = ('uniform', 0, 0.015)
-compose_scene.corals_chance = 0.0
-compose_scene.seaweed_chance = 0.0
-compose_scene.seashells_chance = 0.0
-compose_scene.urchin_chance = 0.0
-compose_scene.jellyfish_chance = 0.0
+compose_nature.corals_chance = 0.0
+compose_nature.seaweed_chance = 0.0
+compose_nature.seashells_chance = 0.0
+compose_nature.urchin_chance = 0.0
+compose_nature.jellyfish_chance = 0.0
-compose_scene.mushroom_chance = 0 # TEMP
-compose_scene.pinecone_chance = 0.1
-compose_scene.pine_needle_chance = 0.1
-compose_scene.caustics_chance = 0.0
-compose_scene.decorative_plants_chance = 0.1
+compose_nature.mushroom_chance = 0 # TEMP
+compose_nature.pinecone_chance = 0.1
+compose_nature.pine_needle_chance = 0.1
+compose_nature.caustics_chance = 0.0
+compose_nature.decorative_plants_chance = 0.1
-compose_scene.cached_fire = False
+compose_nature.cached_fire = False
populate_scene.cached_fire = False
-compose_scene.cached_fire_trees_chance= 0
-compose_scene.cached_fire_bushes_chance = 0
-compose_scene.cached_fire_boulders_chance = 0.0
-compose_scene.cached_fire_cactus_chance = 0
+compose_nature.cached_fire_trees_chance= 0
+compose_nature.cached_fire_bushes_chance = 0
+compose_nature.cached_fire_boulders_chance = 0.0
+compose_nature.cached_fire_cactus_chance = 0
@@ -183,8 +183,8 @@ camera.spawn_camera_rigs.camera_rig_config = [
{'loc': (0.075, 0, 0), 'rot_euler': (0, 0, 0)}
]
-camera_selection_tags_ratio.liquid = (0, 0.5)
-camera_selection_keep_in_animation.liquid = True
+compose_nature.camera_selection_tags_ratio = {"liquid": (0, 0.5)} # often overridden by scenetypes
+compose_nature.camera_selection_anim_criterion_keys = {"liquid": True}
# TERRAIN SEED #
assets.materials.ice.geo_ice.random_seed = %OVERALL_SEED
@@ -201,16 +201,16 @@ assets.materials.mountain.shader.random_seed = %OVERALL_SEED
assets.materials.sand.shader.random_seed = %OVERALL_SEED
assets.materials.water.shader.random_seed = %OVERALL_SEED
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.ground_creature_registry = [
(@CarnivoreFactory, 1),
(@HerbivoreFactory, 1),
(@BirdFactory, 1),
(@SnakeFactory, 1)
]
-compose_scene.flying_creatures_chance=0.1
-compose_scene.flying_creature_registry = [
+compose_nature.flying_creatures_chance=0.1
+compose_nature.flying_creature_registry = [
(@FlyingBirdFactory, 1),
(@DragonflyFactory, 0.1),
]
@@ -225,6 +225,6 @@ group_collections.config = [
{'name': 'animhelper', 'hide_viewport': False, 'hide_render': True}, # curves and iks
]
-include 'base_surface_registry.gin'
-include 'natural.gin'
+include 'infinigen_examples/configs_nature/base_surface_registry.gin'
+include 'infinigen_examples/configs_nature/natural.gin'
diff --git a/infinigen_examples/configs_nature/base_surface_registry.gin b/infinigen_examples/configs_nature/base_surface_registry.gin
new file mode 100644
index 000000000..d224ddcf0
--- /dev/null
+++ b/infinigen_examples/configs_nature/base_surface_registry.gin
@@ -0,0 +1,67 @@
+surface.registry.ground_collection = [
+ ('mud', 2),
+ ('sand', 1),
+ ('cobble_stone', 1),
+ ('cracked_ground', 1),
+ ('dirt', 1),
+ ('stone', 1),
+ ('soil', 1),
+ ('chunkyrock', 0),
+]
+
+surface.registry.beach = [
+ ('sand', 10),
+ ('cracked_ground', 1),
+ ('dirt', 1),
+ ('stone', 1),
+ ('soil', 1),
+]
+
+surface.registry.eroded = [
+ ('sand', 1),
+ ('cracked_ground', 1),
+ ('dirt', 1),
+ ('stone', 1),
+ ('soil', 1),
+]
+
+surface.registry.mountain_collection = [
+ ('mountain', 10),
+ ("sandstone", 2),
+]
+
+surface.registry.rock_collection = [
+ # ('aluminumdisp2tut', 0.5),
+ ('stone', 1),
+ ('mountain', 5),
+ # ('ice', 1),
+]
+
+surface.registry.liquid_collection = [
+ ('water', 0.95),
+ # ('lava', 0.05),
+]
+
+surface.registry.lava = [
+ ('lava', 1),
+]
+
+surface.registry.snow = [
+ ('snow', 1),
+]
+
+surface.registry.atmosphere = [
+ ('atmosphere_light_haze', 1),
+]
+
+surface.registry.bark = [
+ ('bark_birch', 0.1),
+ ('bark_random', 0.9),
+ #('wood', 0.01),
+]
+
+surface.registry.greenery = [
+ ('simple_greenery', 1),
+]
+
+surface.registry.smooth_categories = 0
diff --git a/infinigen_examples/configs_nature/disable_assets/no_assets.gin b/infinigen_examples/configs_nature/disable_assets/no_assets.gin
new file mode 100644
index 000000000..b835452b5
--- /dev/null
+++ b/infinigen_examples/configs_nature/disable_assets/no_assets.gin
@@ -0,0 +1,47 @@
+compose_nature.fancy_clouds_chance = 0.0
+
+compose_nature.trees_chance = 0.0
+compose_nature.bushes_chance = 0.0
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.flying_creatures_chance = 0.0
+compose_nature.clouds_chance = 0.0
+compose_nature.boulders_chance = 0.0
+compose_nature.glowing_rocks_chance = 0.0
+compose_nature.rocks_chance = 0.0
+compose_nature.ground_leaves_chance = 0.0
+compose_nature.ground_twigs_chance = 0.0
+compose_nature.chopped_trees_chance = 0.0
+compose_nature.grass_chance = 0.0
+compose_nature.flowers_chance = 0.0
+compose_nature.kelp_chance = 0.0
+compose_nature.cactus_chance = 0.0
+compose_nature.rain_particles_chance = 0.0
+compose_nature.snow_particles_chance = 0.0
+compose_nature.leaf_particles_chance = 0.0
+compose_nature.dust_particles_chance = 0.0
+compose_nature.camera_based_lighting_chance = 0.0
+compose_nature.wind_chance = 0.0
+compose_nature.turbulence_chance = 0.0
+compose_nature.ferns_chance = 0.0
+
+compose_nature.fish_school_chance = 0.0
+compose_nature.marine_snow_particles_chance = 0.0
+
+compose_nature.corals_chance = 0.0
+compose_nature.seaweed_chance = 0.0
+compose_nature.urchin_chance = 0.0
+compose_nature.seashells_chance = 0.0
+compose_nature.jellyfish_chance = 0.0
+compose_nature.mushroom_chance = 0.0
+compose_nature.pinecone_chance = 0.0
+compose_nature.monocots_chance = 0.0
+compose_nature.pine_needle_chance = 0.0
+compose_nature.caustics_chance = 0.0
+
+compose_nature.decorative_plants_chance = 0.0
+
+populate_scene.moss_chance = 0.0
+populate_scene.lichen_chance = 0.0
+populate_scene.slime_mold_chance = 0.0
+populate_scene.ivy_chance = 0.0
+populate_scene.snow_layer_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/disable_assets/no_creatures.gin b/infinigen_examples/configs_nature/disable_assets/no_creatures.gin
new file mode 100644
index 000000000..1546e3aab
--- /dev/null
+++ b/infinigen_examples/configs_nature/disable_assets/no_creatures.gin
@@ -0,0 +1,4 @@
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.flying_creatures_chance = 0.0
+compose_nature.fish_school_chance = 0.0
+compose_nature.bug_swarm_chance = 0.0
diff --git a/infinigen_examples/configs_nature/disable_assets/no_particles.gin b/infinigen_examples/configs_nature/disable_assets/no_particles.gin
new file mode 100644
index 000000000..7e2dd2b28
--- /dev/null
+++ b/infinigen_examples/configs_nature/disable_assets/no_particles.gin
@@ -0,0 +1,5 @@
+compose_nature.rain_particles_chance = 0.0
+compose_nature.snow_particles_chance = 0.0
+compose_nature.leaf_particles_chance = 0.0
+compose_nature.dust_particles_chance = 0.0
+compose_nature.marine_snow_particles_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/disable_assets/no_rocks.gin b/infinigen_examples/configs_nature/disable_assets/no_rocks.gin
new file mode 100644
index 000000000..aa630414e
--- /dev/null
+++ b/infinigen_examples/configs_nature/disable_assets/no_rocks.gin
@@ -0,0 +1,2 @@
+compose_nature.boulders_chance = 0.0
+compose_nature.rocks_chance = 0.0
\ No newline at end of file
diff --git a/infinigen_examples/configs/extras/experimental.gin b/infinigen_examples/configs_nature/extras/experimental.gin
similarity index 56%
rename from infinigen_examples/configs/extras/experimental.gin
rename to infinigen_examples/configs_nature/extras/experimental.gin
index cba3d47fd..f82010755 100644
--- a/infinigen_examples/configs/extras/experimental.gin
+++ b/infinigen_examples/configs_nature/extras/experimental.gin
@@ -1,7 +1,7 @@
# things that are not quite fully working correctly, but you can use if you please
configure_blender.motion_blur = True # not fully supported in ground truth
-compose_scene.rain_particles_chance = 0.1 # doesnt look good when not using motion blur
+compose_nature.rain_particles_chance = 0.1 # doesnt look good when not using motion blur
-# compose_scene.marine_snow_particles_chance = 0.1 # TODO only put this in underwater scenes
+# compose_nature.marine_snow_particles_chance = 0.1 # TODO only put this in underwater scenes
# water.is_ocean = ("bool", 0.5) # TODO put this only in underwater scenes
diff --git a/infinigen_examples/configs/extras/overhead.gin b/infinigen_examples/configs_nature/extras/overhead.gin
similarity index 59%
rename from infinigen_examples/configs/extras/overhead.gin
rename to infinigen_examples/configs_nature/extras/overhead.gin
index 028b8295a..869c87e76 100644
--- a/infinigen_examples/configs/extras/overhead.gin
+++ b/infinigen_examples/configs_nature/extras/overhead.gin
@@ -3,8 +3,8 @@ camera.camera_pose_proposal.altitude = ("clip_gaussian", 30, 20, 17, 70)
camera.camera_pose_proposal.pitch = ("clip_gaussian", 0, 30, 0, 15)
placement.populate_all.dist_cull = 70
-compose_scene.inview_distance = 70
-compose_scene.near_distance = 40
-compose_scene.center_distance = 40
+compose_nature.inview_distance = 70
+compose_nature.near_distance = 40
+compose_nature.center_distance = 40
-compose_scene.animate_cameras_enabled = False
\ No newline at end of file
+compose_nature.animate_cameras_enabled = False
\ No newline at end of file
diff --git a/infinigen_examples/configs/extras/stereo_training.gin b/infinigen_examples/configs_nature/extras/stereo_training.gin
similarity index 86%
rename from infinigen_examples/configs/extras/stereo_training.gin
rename to infinigen_examples/configs_nature/extras/stereo_training.gin
index 1d5d989ba..6f47d6a8a 100644
--- a/infinigen_examples/configs/extras/stereo_training.gin
+++ b/infinigen_examples/configs_nature/extras/stereo_training.gin
@@ -8,9 +8,9 @@ atmosphere_light_haze.shader_atmosphere.enable_scatter = False
water.shader.enable_scatter = False
# dont include tiny particles, they arent sufficiently visible
-compose_scene.rain_particles_chance = 0
-compose_scene.dust_particles_chance = 0
-compose_scene.snow_particles_chance = 0
+compose_nature.rain_particles_chance = 0
+compose_nature.dust_particles_chance = 0
+compose_nature.snow_particles_chance = 0
# eliminate lava, the emissive surface is too noisy
# now by default lava is a separate material
diff --git a/infinigen_examples/configs/extras/use_cached_fire.gin b/infinigen_examples/configs_nature/extras/use_cached_fire.gin
similarity index 62%
rename from infinigen_examples/configs/extras/use_cached_fire.gin
rename to infinigen_examples/configs_nature/extras/use_cached_fire.gin
index 0c1750088..bbb37a35e 100644
--- a/infinigen_examples/configs/extras/use_cached_fire.gin
+++ b/infinigen_examples/configs_nature/extras/use_cached_fire.gin
@@ -4,14 +4,14 @@ populate_scene.creatures_fire_on_the_fly_chance = 0
populate_scene.boulders_fire_on_the_fly_chance = 0
populate_scene.cactus_fire_on_the_fly_chance = 0
-compose_scene.cached_fire_trees_chance= 0.5
-compose_scene.cached_fire_bushes_chance = 1
-compose_scene.cached_fire_boulders_chance = 0.3
-compose_scene.cached_fire_cactus_chance = 0.4
+compose_nature.cached_fire_trees_chance= 0.5
+compose_nature.cached_fire_bushes_chance = 1
+compose_nature.cached_fire_boulders_chance = 0.3
+compose_nature.cached_fire_cactus_chance = 0.4
configure_render_cycles.exposure = 0.4
-compose_scene.cached_fire = True
+compose_nature.cached_fire = True
populate_scene.cached_fire = True
FireCachingSystem.asset_folder = ""
\ No newline at end of file
diff --git a/infinigen_examples/configs/extras/use_on_the_fly_fire.gin b/infinigen_examples/configs_nature/extras/use_on_the_fly_fire.gin
similarity index 76%
rename from infinigen_examples/configs/extras/use_on_the_fly_fire.gin
rename to infinigen_examples/configs_nature/extras/use_on_the_fly_fire.gin
index 625c1150b..c2959782e 100644
--- a/infinigen_examples/configs/extras/use_on_the_fly_fire.gin
+++ b/infinigen_examples/configs_nature/extras/use_on_the_fly_fire.gin
@@ -1,13 +1,13 @@
populate_scene.trees_fire_on_the_fly_chance = 0.1
-#compose_scene.trees_chance = 1
+#compose_nature.trees_chance = 1
populate_scene.bushes_fire_on_the_fly_chance = 0.8
-#compose_scene.bushes_chance = 1
+#compose_nature.bushes_chance = 1
populate_scene.creatures_fire_on_the_fly_chance = 0
populate_scene.boulders_fire_on_the_fly_chance = 0.1
populate_scene.cactus_fire_on_the_fly_chance = 0.1
-compose_scene.glowing_rocks_chance = 0.0
+compose_nature.glowing_rocks_chance = 0.0
-compose_scene.cached_fire = False
+compose_nature.cached_fire = False
LandTiles.land_process = None
#scene.voronoi_rocks_chance = 1
#animate_cameras.follow_poi_chance=0
diff --git a/infinigen_examples/configs/monocular.gin b/infinigen_examples/configs_nature/monocular.gin
similarity index 100%
rename from infinigen_examples/configs/monocular.gin
rename to infinigen_examples/configs_nature/monocular.gin
diff --git a/infinigen_examples/configs_nature/natural.gin b/infinigen_examples/configs_nature/natural.gin
new file mode 100644
index 000000000..67ce4ac03
--- /dev/null
+++ b/infinigen_examples/configs_nature/natural.gin
@@ -0,0 +1,3 @@
+# assets.materials.water.shader.color = ("palette", "water")
+assets.materials.mountain.shader.color = ("palette", "mountain soil")
+assets.materials.sandstone.shader.color = ("palette", "sandstone")
\ No newline at end of file
diff --git a/infinigen_examples/configs/noisy_video.gin b/infinigen_examples/configs_nature/noisy_video.gin
similarity index 100%
rename from infinigen_examples/configs/noisy_video.gin
rename to infinigen_examples/configs_nature/noisy_video.gin
diff --git a/infinigen_examples/configs/palette/desert.json b/infinigen_examples/configs_nature/palette/desert.json
similarity index 100%
rename from infinigen_examples/configs/palette/desert.json
rename to infinigen_examples/configs_nature/palette/desert.json
diff --git a/infinigen_examples/configs/palette/mountain soil.json b/infinigen_examples/configs_nature/palette/mountain soil.json
similarity index 100%
rename from infinigen_examples/configs/palette/mountain soil.json
rename to infinigen_examples/configs_nature/palette/mountain soil.json
diff --git a/infinigen_examples/configs/palette/sandstone.json b/infinigen_examples/configs_nature/palette/sandstone.json
similarity index 100%
rename from infinigen_examples/configs/palette/sandstone.json
rename to infinigen_examples/configs_nature/palette/sandstone.json
diff --git a/infinigen_examples/configs/palette/water.json b/infinigen_examples/configs_nature/palette/water.json
similarity index 100%
rename from infinigen_examples/configs/palette/water.json
rename to infinigen_examples/configs_nature/palette/water.json
diff --git a/infinigen_examples/configs/performance/dev.gin b/infinigen_examples/configs_nature/performance/dev.gin
similarity index 61%
rename from infinigen_examples/configs/performance/dev.gin
rename to infinigen_examples/configs_nature/performance/dev.gin
index ad0fed64b..413ff7e34 100644
--- a/infinigen_examples/configs/performance/dev.gin
+++ b/infinigen_examples/configs_nature/performance/dev.gin
@@ -7,10 +7,10 @@ OpaqueSphericalMesher.pixels_per_cube = 4
TransparentSphericalMesher.pixels_per_cube = 4
target_face_size.global_multiplier = 2
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.flying_creatures_chance = 0.0
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.flying_creatures_chance = 0.0
-compose_scene.inview_distance = 35
+compose_nature.inview_distance = 35
placement.populate_all.dist_cull = 35
-compose_scene.near_distance = 10
-compose_scene.center_distance = 20
\ No newline at end of file
+compose_nature.near_distance = 10
+compose_nature.center_distance = 20
\ No newline at end of file
diff --git a/infinigen_examples/configs/performance/fast_terrain_assets.gin b/infinigen_examples/configs_nature/performance/fast_terrain_assets.gin
similarity index 100%
rename from infinigen_examples/configs/performance/fast_terrain_assets.gin
rename to infinigen_examples/configs_nature/performance/fast_terrain_assets.gin
diff --git a/infinigen_examples/configs/performance/high_quality_terrain.gin b/infinigen_examples/configs_nature/performance/high_quality_terrain.gin
similarity index 100%
rename from infinigen_examples/configs/performance/high_quality_terrain.gin
rename to infinigen_examples/configs_nature/performance/high_quality_terrain.gin
diff --git a/infinigen_examples/configs/performance/reuse_terrain_assets.gin b/infinigen_examples/configs_nature/performance/reuse_terrain_assets.gin
similarity index 100%
rename from infinigen_examples/configs/performance/reuse_terrain_assets.gin
rename to infinigen_examples/configs_nature/performance/reuse_terrain_assets.gin
diff --git a/infinigen_examples/configs_nature/performance/simple.gin b/infinigen_examples/configs_nature/performance/simple.gin
new file mode 100644
index 000000000..e5ec006cf
--- /dev/null
+++ b/infinigen_examples/configs_nature/performance/simple.gin
@@ -0,0 +1,5 @@
+include 'infinigen_examples/configs_nature/performance/dev.gin'
+include 'infinigen_examples/configs_nature/disable_assets/no_creatures.gin'
+include 'infinigen_examples/configs_nature/performance/fast_terrain_assets.gin'
+run_erosion.n_iters = [1,1]
+full/configure_render_cycles.num_samples = 100
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/arctic.gin b/infinigen_examples/configs_nature/scene_types/arctic.gin
similarity index 76%
rename from infinigen_examples/configs/scene_types/arctic.gin
rename to infinigen_examples/configs_nature/scene_types/arctic.gin
index 57bcb33d2..f0c337735 100644
--- a/infinigen_examples/configs/scene_types/arctic.gin
+++ b/infinigen_examples/configs_nature/scene_types/arctic.gin
@@ -6,7 +6,7 @@ surface.registry.rock_collection = [
('ice', 1),
]
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creature_registry = [
(@CarnivoreFactory, 0),
(@HerbivoreFactory, 0.3),
(@BirdFactory, 1),
@@ -29,12 +29,12 @@ scene.warped_rocks_chance = 0
scene.ground_ice_chance = 1
scene.waterbody_chance = 1
-include 'disable_assets/no_assets.gin'
+include 'infinigen_examples/configs_nature/disable_assets/no_assets.gin'
-compose_scene.wind_chance = 0.5
-compose_scene.turbulence_chance = 0.5
-compose_scene.boulders_chance = 0.3
-compose_scene.rocks_chance = 0.3
+compose_nature.wind_chance = 0.5
+compose_nature.turbulence_chance = 0.5
+compose_nature.boulders_chance = 0.3
+compose_nature.rocks_chance = 0.3
shader_atmosphere.density = 0
water.geo.water_height = ("uniform", 0.002, 0.004)
diff --git a/infinigen_examples/configs/scene_types/canyon.gin b/infinigen_examples/configs_nature/scene_types/canyon.gin
similarity index 62%
rename from infinigen_examples/configs/scene_types/canyon.gin
rename to infinigen_examples/configs_nature/scene_types/canyon.gin
index efa40eb25..ab4f3fdc3 100644
--- a/infinigen_examples/configs/scene_types/canyon.gin
+++ b/infinigen_examples/configs_nature/scene_types/canyon.gin
@@ -7,9 +7,11 @@ LandTiles.randomness = 1
# camera selection config
keep_cam_pose_proposal.terrain_coverage_range = (0.5, 0.9)
-camera_selection_ranges_ratio.altitude = ("altitude", 16, 1e9, 0.01, 1)
+compose_nature.camera_selection_ranges_ratio = {
+ "altitude": ("altitude", 16, 1e9, 0.01, 1)
+}
-compose_scene.ground_creatures_chance = 0.2
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creatures_chance = 0.2
+compose_nature.ground_creature_registry = [
(@SnakeFactory, 0.9)
]
diff --git a/infinigen_examples/configs_nature/scene_types/cave.gin b/infinigen_examples/configs_nature/scene_types/cave.gin
new file mode 100644
index 000000000..fe15e98db
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/cave.gin
@@ -0,0 +1,65 @@
+compose_nature.land_domain_tags = 'landscape,-liquid_covered'
+compose_nature.nonliving_domain_tags = 'landscape'
+compose_nature.underwater_domain_tags = 'landscape,liquid_covered'
+
+compose_nature.trees_chance = 0.4
+compose_nature.rocks_chance = 0.8
+compose_nature.glowing_rocks_chance = 1
+compose_nature.grass_chance = 0.4
+compose_nature.ferns_chance = 0.6
+compose_nature.mushroom_chance = 0.8
+
+compose_nature.snow_particles_chance=0.0
+compose_nature.leaves_chance=0.0
+compose_nature.rain_particles_chance=0.0
+
+surface.registry.liquid_collection = [
+ ('water', 0.7),
+ ('lava', 0.3)
+]
+
+compose_nature.bug_swarm_chance=0.0
+
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.ground_creature_registry = [
+ (@CarnivoreFactory, 1),
+ (@HerbivoreFactory, 0.3),
+ (@BirdFactory, 0.5),
+ (@BeetleFactory, 1)
+]
+
+compose_nature.flying_creatures_chance=0.2
+compose_nature.flying_creature_registry = [
+ (@DragonflyFactory, 1),
+]
+
+atmosphere_light_haze.shader_atmosphere.enable_scatter = False
+configure_render_cycles.exposure = 1.3
+
+
+# scene composition config
+scene.caves_chance = 1
+scene.ground_chance = 0.5
+Caves.randomness = 0
+Caves.height_offset = -4
+Caves.frequency = 0.01
+Caves.noise_scale = ("uniform", 2, 5)
+Caves.n_lattice = 1
+Caves.is_horizontal = 1
+Caves.scale_increase = 1
+
+scene.waterbody_chance = 0.8
+Waterbody.height = -5
+
+# camera selection config
+Terrain.populated_bounds = (-25, 25, -25, 25, -25, 0)
+keep_cam_pose_proposal.terrain_coverage_range = (1, 1)
+compose_nature.camera_selection_ranges_ratio = {
+ "closeup": ("closeup", 4, 0, 0.3)
+}
+compose_nature.camera_selection_tags_ratio = {
+ "liquid": (0, 0.5),
+ "cave": (0.3, 1)
+}
+
+SphericalMesher.r_min = 0.2
diff --git a/infinigen_examples/configs/scene_types/cliff.gin b/infinigen_examples/configs_nature/scene_types/cliff.gin
similarity index 67%
rename from infinigen_examples/configs/scene_types/cliff.gin
rename to infinigen_examples/configs_nature/scene_types/cliff.gin
index 2092e6def..346a52db9 100644
--- a/infinigen_examples/configs/scene_types/cliff.gin
+++ b/infinigen_examples/configs_nature/scene_types/cliff.gin
@@ -1,4 +1,4 @@
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creature_registry = [
(@CarnivoreFactory, 0.2),
(@HerbivoreFactory, 1),
(@BirdFactory, 1),
@@ -15,9 +15,11 @@ Ground.height = -15
# camera selection config
Terrain.populated_bounds = (-25, 25, -25, 25, -15, 35)
keep_cam_pose_proposal.terrain_coverage_range = (0, 0.8)
-camera_selection_ranges_ratio.altitude = ("altitude", 10, 1e9, 0.01, 1)
+compose_nature.camera_selection_ranges_ratio = {
+ "altitude": ("altitude", 10, 1e9, 0.01, 1)
+}
-compose_scene.flying_creatures_chance=0.6
-compose_scene.flying_creature_registry = [
+compose_nature.flying_creatures_chance=0.6
+compose_nature.flying_creature_registry = [
(@FlyingBirdFactory, 1),
]
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types/coast.gin b/infinigen_examples/configs_nature/scene_types/coast.gin
similarity index 73%
rename from infinigen_examples/configs/scene_types/coast.gin
rename to infinigen_examples/configs_nature/scene_types/coast.gin
index b17b52154..9371c782b 100644
--- a/infinigen_examples/configs/scene_types/coast.gin
+++ b/infinigen_examples/configs_nature/scene_types/coast.gin
@@ -29,16 +29,18 @@ shader_atmosphere.anisotropy = 1
shader_atmosphere.density = 0
# camera selection config
-camera_selection_tags_ratio.liquid = (0.05, 0.6)
-camera_selection_tags_ratio.beach = (0.05, 0.6)
+compose_nature.camera_selection_tags_ratio = {
+ "liquid": (0.05, 0.6),
+ "beach": (0.05, 0.6),
+}
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.ground_creature_registry = [
(@BirdFactory, 0.1),
(@CrabFactory, 1)
]
-compose_scene.max_ground_creatures = 8
-compose_scene.flying_creatures_chance=0.6
-compose_scene.flying_creature_registry = [
+compose_nature.max_ground_creatures = 8
+compose_nature.flying_creatures_chance=0.6
+compose_nature.flying_creature_registry = [
(@FlyingBirdFactory, 1)
]
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/scene_types/coral_reef.gin b/infinigen_examples/configs_nature/scene_types/coral_reef.gin
new file mode 100644
index 000000000..12917127d
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/coral_reef.gin
@@ -0,0 +1,11 @@
+include 'infinigen_examples/configs_nature/scene_types/under_water.gin'
+
+compose_nature.kelp_chance = 0.1
+compose_nature.urchin_chance = 0.1
+
+compose_nature.corals_chance = 1.
+compose_nature.seaweed_chance = 0.2
+compose_nature.seashell_chance = 0.7
+compose_nature.jellyfish_chance = 0.0
+
+water.geo.with_waves=True
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/scene_types/desert.gin b/infinigen_examples/configs_nature/scene_types/desert.gin
new file mode 100644
index 000000000..80fca8ff8
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/desert.gin
@@ -0,0 +1,46 @@
+
+compose_nature.trees_chance = 0.25
+compose_nature.cactus_chance = .7
+compose_nature.ground_leaves_chance = 0.5
+compose_nature.ground_twigs_chance = 0.3
+compose_nature.chopped_trees_chance = 0.1
+compose_nature.grass_chance = 0.1
+compose_nature.ferns_chance = 0.0
+compose_nature.pine_needle_chance = 0.05
+compose_nature.fancy_clouds_chance = 0.0
+
+compose_nature.tree_density = 0.02
+
+compose_nature.rain_particles_chance = 0.0
+compose_nature.snow_particles_chance = 0
+compose_nature.leaf_particles_chance = 0.05
+compose_nature.dust_particles_chance = 0.0
+
+atmosphere_light_haze.shader_atmosphere.density = ("uniform", 0, 0.0015)
+atmosphere_light_haze.shader_atmosphere.anisotropy = 0
+
+animate_cameras.follow_poi_chance=0.5
+
+compose_nature.ground_creatures_chance = 1.0
+compose_nature.ground_creature_registry = [
+ (@SnakeFactory, 1)
+]
+
+surface.registry.ground_collection = [
+ ('sand', 1),
+]
+
+surface.registry.mountain_collection = [
+ ("sandstone", 1),
+]
+
+# scene composition config
+LandTiles.tiles = ["Mountain"]
+LandTiles.randomness = 1
+LandTiles.tile_heights = [-2]
+LandTiles.tile_density = 0.25
+WarpedRocks.slope_shift = -3
+Ground.with_sand_dunes = 1
+
+
+scene.waterbody_chance = 0
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/scene_types/forest.gin b/infinigen_examples/configs_nature/scene_types/forest.gin
new file mode 100644
index 000000000..3e2838b00
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/forest.gin
@@ -0,0 +1,58 @@
+Ground.scale = 10
+multi_mountains_params.height = 15
+multi_mountains_params.min_freq = ("uniform", 0.008, 0.012)
+multi_mountains_params.max_freq = ("uniform", 0.024, 0.036)
+scene.warped_rocks_chance = 0
+
+compose_nature.inview_distance = 40
+placement.populate_all.dist_cull = 40
+
+surface.registry.ground_collection = [
+ ('mud', 2),
+ ('dirt', 1),
+ ('soil', 1),
+]
+
+surface.registry.mountain_collection = [
+ ('dirt', 1),
+ ('soil', 1),
+ ('cracked_ground', 0.5)
+]
+
+compose_nature.ground_creatures_chance = 0.1
+compose_nature.ground_creature_registry = [
+ #(@CarnivoreFactory, 2),
+ #(@HerbivoreFactory, 0.8),
+ (@SnakeFactory, 2)
+]
+
+compose_nature.flying_creatures_chance = 0.7
+compose_nature.flying_creature_registry = [
+ (@DragonflyFactory, 1),
+ (@FlyingBirdFactory, 0.2)
+]
+
+compose_nature.bug_swarm_chance = 0.0
+
+compose_nature.trees_chance = 1.0
+compose_nature.tree_density = 0.11
+
+compose_nature.ground_leaves_chance = 0.7
+compose_nature.ground_twigs_chance = 0.7
+compose_nature.chopped_trees_chance = 0.3
+compose_nature.grass_chance = 0.4
+compose_nature.ferns_chance = 0.6
+compose_nature.flowers_chance = 0.4
+compose_nature.monocots_chance = 0.5
+compose_nature.mushroom_chance = 0.3
+compose_nature.pinecone_chance = 0.5
+compose_nature.pine_needle_chance = 0.6
+
+populate_scene.slime_mold_chance = 0.0
+populate_scene.ivy_chance = 0.0
+populate_scene.lichen_chance = 0.6
+populate_scene.mushroom_chance = 0.0
+populate_scene.moss_chance = 0.0
+
+compose_nature.rain_particles_chance = 0.0
+compose_nature.leaf_particles_chance = 0.7
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/scene_types/kelp_forest.gin b/infinigen_examples/configs_nature/scene_types/kelp_forest.gin
new file mode 100644
index 000000000..55acc05e3
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/kelp_forest.gin
@@ -0,0 +1,24 @@
+include 'infinigen_examples/configs_nature/scene_types/under_water.gin'
+
+multi_mountains_params.height = ("uniform", 1, 4)
+multi_mountains_params.min_freq = ("uniform", 0.01, 0.015)
+multi_mountains_params.max_freq = ("uniform", 0.03, 0.06)
+
+compose_nature.glowing_rocks_chance = 0.
+compose_nature.ground_leaves_chance = 0.2
+compose_nature.ground_twigs_chance = 0.4
+compose_nature.chopped_trees_chance = 0.
+
+compose_nature.kelp_chance = 1.0
+compose_nature.urchin_chance = 0.7
+
+compose_nature.corals_chance = 0.0
+compose_nature.seaweed_chance = 0.8
+compose_nature.seashell_chance = 0.8
+compose_nature.jellyfish_chance = 0.0
+
+water.shader.volume_density = ("uniform", 0.09, 0.13)
+water.shader.anisotropy = ("uniform", 0.45, 0.7)
+water.geo.with_waves=False
+
+camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 15, 60, 140)
diff --git a/infinigen_examples/configs/scene_types/mountain.gin b/infinigen_examples/configs_nature/scene_types/mountain.gin
similarity index 55%
rename from infinigen_examples/configs/scene_types/mountain.gin
rename to infinigen_examples/configs_nature/scene_types/mountain.gin
index 052535055..7a8696c0e 100644
--- a/infinigen_examples/configs/scene_types/mountain.gin
+++ b/infinigen_examples/configs_nature/scene_types/mountain.gin
@@ -1,10 +1,10 @@
-compose_scene.flying_creatures_chance=0.7
-compose_scene.max_flying_creatures = 3
+compose_nature.flying_creatures_chance=0.7
+compose_nature.max_flying_creatures = 3
animate_cameras.follow_poi_chance=0.0
-compose_scene.trees_chance = 0.5
-compose_scene.tree_density = 0.06
+compose_nature.trees_chance = 0.5
+compose_nature.tree_density = 0.06
# scene composition config
scene.upsidedown_mountains_chance = 0.4
diff --git a/infinigen_examples/configs_nature/scene_types/plain.gin b/infinigen_examples/configs_nature/scene_types/plain.gin
new file mode 100644
index 000000000..8162b6679
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/plain.gin
@@ -0,0 +1,28 @@
+compose_nature.ground_creature_registry = [
+ (@CarnivoreFactory, 1),
+ (@HerbivoreFactory, 1.5),
+ (@BirdFactory, 0.7),
+]
+
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.ground_creature_registry = [
+ (@CarnivoreFactory, 2),
+ (@HerbivoreFactory, 0.8),
+ (@SnakeFactory, 2)
+]
+
+compose_nature.flying_creatures_chance = 0.7
+compose_nature.flying_creature_registry = [
+ (@DragonflyFactory, 1),
+ (@FlyingBirdFactory, 0.1)
+]
+
+compose_nature.grass_select_max = 0.35
+compose_nature.tree_density = 0.02
+compose_nature.grass_chance = 1
+
+compose_nature.bug_swarm_chance=0.0
+
+# scene composition config
+scene.landtiles_chance = 0
+scene.warped_rocks_chance = 0
diff --git a/infinigen_examples/configs/scene_types/river.gin b/infinigen_examples/configs_nature/scene_types/river.gin
similarity index 67%
rename from infinigen_examples/configs/scene_types/river.gin
rename to infinigen_examples/configs_nature/scene_types/river.gin
index 52a7c1654..c753c31c3 100644
--- a/infinigen_examples/configs/scene_types/river.gin
+++ b/infinigen_examples/configs_nature/scene_types/river.gin
@@ -6,20 +6,20 @@ surface.registry.erosion_collection = [
('soil', 1),
]
-compose_scene.ground_creatures_chance = 0.0
-compose_scene.ground_creature_registry = [
+compose_nature.ground_creatures_chance = 0.0
+compose_nature.ground_creature_registry = [
(@CarnivoreFactory, 2),
(@HerbivoreFactory, 0.5),
(@SnakeFactory, 0.5)
]
-compose_scene.flying_creatures_chance = 0.7
-compose_scene.flying_creature_registry = [
+compose_nature.flying_creatures_chance = 0.7
+compose_nature.flying_creature_registry = [
(@DragonflyFactory, 1),
(@FlyingBirdFactory, 0.2)
]
-compose_scene.rain_particles_chance = 0.0
+compose_nature.rain_particles_chance = 0.0
populate_scene.slime_mold_chance = 0.3
populate_scene.ivy_chance = 0.3
populate_scene.lichen_chance = 0.3
@@ -38,5 +38,9 @@ scene.warped_rocks_chance = 0
# camera selection config
Terrain.populated_bounds = (-25, 25, -25, 25, -15, 35)
-camera_selection_ranges_ratio.altitude = ("altitude", -1e9, 0.75, 0.1, 1)
-camera_selection_tags_ratio.liquid = (0.05, 1)
+compose_nature.camera_selection_ranges_ratio = {
+ "altitude": ("altitude", -1e9, 0.75, 0.1, 1)
+}
+compose_nature.camera_selection_tags_ratio = {
+ "liquid": (0.05, 1)
+}
diff --git a/infinigen_examples/configs/scene_types/snowy_mountain.gin b/infinigen_examples/configs_nature/scene_types/snowy_mountain.gin
similarity index 77%
rename from infinigen_examples/configs/scene_types/snowy_mountain.gin
rename to infinigen_examples/configs_nature/scene_types/snowy_mountain.gin
index 09737c895..b9ea8d81f 100644
--- a/infinigen_examples/configs/scene_types/snowy_mountain.gin
+++ b/infinigen_examples/configs_nature/scene_types/snowy_mountain.gin
@@ -1,4 +1,4 @@
-include 'disable_assets/no_assets.gin'
+include 'infinigen_examples/configs_nature/disable_assets/no_assets.gin'
surface.registry.rock_collection = [
('mountain', 1),
@@ -7,7 +7,7 @@ surface.registry.mountain_collection = [
('mountain', 1),
]
-compose_scene.snow_particles_chance = 0.5
+compose_nature.snow_particles_chance = 0.5
shader_atmosphere.density = 0
nishita_lighting.sun_elevation = ("spherical_sample", 10, 30)
@@ -21,14 +21,14 @@ scene.voronoi_grains_chance = 0
tile_directions.MultiMountains = "initial"
-compose_scene.flying_creatures_chance = 0.5
-compose_scene.flying_creature_registry = [
+compose_nature.flying_creatures_chance = 0.5
+compose_nature.flying_creature_registry = [
(@FlyingBirdFactory, 1),
]
assets.materials.mountain.shader.layered_mountain = 0
assets.materials.mountain.shader.snowy = 0 # TODO: re-enable once terrain flickering resolved
-compose_scene.boulders_chance = 1
+compose_nature.boulders_chance = 1
camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 30, 90, 100)
keep_cam_pose_proposal.terrain_coverage_range = (0.3, 1)
diff --git a/infinigen_examples/configs_nature/scene_types/under_water.gin b/infinigen_examples/configs_nature/scene_types/under_water.gin
new file mode 100644
index 000000000..551914e95
--- /dev/null
+++ b/infinigen_examples/configs_nature/scene_types/under_water.gin
@@ -0,0 +1,82 @@
+multi_mountains_params.height = ("uniform", 4, 10)
+multi_mountains_params.min_freq = ("uniform", 0.01, 0.015)
+multi_mountains_params.max_freq = ("uniform", 0.03, 0.06)
+
+keep_cam_pose_proposal.terrain_coverage_range = (0.4, 1)
+camera.camera_pose_proposal.altitude = ("clip_gaussian", 3.5, 0.7, 2, 6)
+camera.camera_pose_proposal.pitch = ("clip_gaussian", 90, 15, 60, 110)
+
+compose_nature.trees_chance = 0.
+compose_nature.bushes_chance = 0.
+compose_nature.creatures_chance = 1.0
+compose_nature.glowing_rocks_chance = 0.05
+compose_nature.rocks_chance = 0.5
+compose_nature.ground_leaves_chance = 0.05
+compose_nature.ground_twigs_chance = 0.05
+compose_nature.chopped_trees_chance = 0.1
+compose_nature.grass_chance = 0.05
+compose_nature.kelp_chance = 0.7
+compose_nature.cactus_chance = 0.
+compose_nature.ferns_chance = 0.01
+compose_nature.camera_based_lighting_chance = 1.0
+
+compose_nature.corals_chance = 0.8
+compose_nature.seaweed_chance = 0.8
+compose_nature.seashell_chance = 0.8
+compose_nature.urchin_chance = 0.8
+compose_nature.jellyfish_chance = 0.0
+compose_nature.mushroom_chance = 0.0
+compose_nature.pinecone_chance = 0.0
+compose_nature.pine_needle_chance = 0.0
+compose_nature.boulders_chance = 0.0
+compose_nature.monocots_chance = 0.0
+
+water.geo.water_detail = ("uniform", 0.2, 0.5)
+water.geo.water_height = ("uniform", 0.05, 0.15)
+water.geo.with_ripples = 0
+water.shader.volume_density = ("uniform", 0.03, 0.05)
+water.shader.color = ("color_category", 'under_water')
+water.shader.colored = 0
+water.shader.emissive_foam = 0
+water.shader.mix_surface = True
+water.is_ocean = False
+
+
+compose_nature.wind_chance = 0
+compose_nature.rain_particles_chance = 0
+compose_nature.snow_particles_chance = 0
+compose_nature.leaf_particles_chance = 0
+compose_nature.dust_particles_chance = 0
+compose_nature.marine_snow_particles_chance = 0
+compose_nature.caustics_chance = 0.6
+
+atmosphere_light_haze.shader_atmosphere.enable_scatter = False
+compose_nature.fancy_clouds_chance=0.0
+
+surface.registry.rock_collection = [
+ ('stone', 1),
+ ('mountain', 0.5),
+]
+
+compose_nature.ground_creatures_chance = 0.4
+compose_nature.ground_creature_registry = [
+ (@CrustaceanFactory, 1),
+]
+compose_nature.flying_creatures_chance = 0.0
+compose_nature.bug_swarm_chance = 0.0
+compose_nature.fish_school_chance = 0.3
+
+# scene composition config
+LandTiles.tile_heights = [-20]
+Ground.height = -20
+Atmosphere.hacky_offset = 0.1
+WarpedRocks.slope_shift = -23
+scene.waterbody_chance = 1
+
+Terrain.under_water = 1
+
+compose_nature.turbulence_chance = 0.7
+turbulence_effector.strength = ("uniform", 0, 7)
+turbulence_effector.size = ("uniform", 1.5, 4.5)
+turbulence_effector.flow = 1
+turbulence_effector.noise = 10
\ No newline at end of file
diff --git a/infinigen_examples/configs/scene_types_fluidsim/simulated_river.gin b/infinigen_examples/configs_nature/scene_types_fluidsim/simulated_river.gin
similarity index 75%
rename from infinigen_examples/configs/scene_types_fluidsim/simulated_river.gin
rename to infinigen_examples/configs_nature/scene_types_fluidsim/simulated_river.gin
index 0e88eb715..f38852e70 100644
--- a/infinigen_examples/configs/scene_types_fluidsim/simulated_river.gin
+++ b/infinigen_examples/configs_nature/scene_types_fluidsim/simulated_river.gin
@@ -1,4 +1,4 @@
-include 'scene_types/river.gin'
+include 'infinigen_examples/configs_nature/scene_types/river.gin'
UniformMesher.enclosed=1
animate_cameras.policy_registry = @cam/AnimPolicyRandomForwardWalk
@@ -9,15 +9,16 @@ cam/AnimPolicyRandomForwardWalk.yaw_dist = ("uniform",-10, 10)
keep_cam_pose_proposal.min_terrain_distance = 1
-compose_scene.hero_boulders_chance = 0.0
-compose_scene.simulated_river_enabled = True
+compose_nature.hero_boulders_chance = 0.0
+compose_nature.simulated_river_enabled = True
terrain.elements.landtiles.LandTiles.y_tilt = 0
terrain.elements.landtiles.LandTiles.y_tilt_clip = 0
camera_pose_proposal.override_loc = (0.45, -24, 8)
-camera_pose_proposal.override_rot = (-120, 180, -178)
-
+camera_pose_proposal.pitch = -120
+camera_pose_proposal.roll = 180
+camera_pose_proposal.yaw = -178
assets.boulder.create_placeholder.boulder_scale = 1
LandTiles.land_process = None
diff --git a/infinigen_examples/configs/scene_types_fluidsim/tilted_river.gin b/infinigen_examples/configs_nature/scene_types_fluidsim/tilted_river.gin
similarity index 56%
rename from infinigen_examples/configs/scene_types_fluidsim/tilted_river.gin
rename to infinigen_examples/configs_nature/scene_types_fluidsim/tilted_river.gin
index 209b5068a..cfa621615 100644
--- a/infinigen_examples/configs/scene_types_fluidsim/tilted_river.gin
+++ b/infinigen_examples/configs_nature/scene_types_fluidsim/tilted_river.gin
@@ -1,4 +1,4 @@
-include 'scene_types/river.gin'
+include 'infinigen_examples/configs_nature/scene_types/river.gin'
UniformMesher.enclosed=1
animate_cameras.policy_registry = @cam/AnimPolicyRandomForwardWalk
@@ -10,18 +10,21 @@ cam/AnimPolicyRandomForwardWalk.yaw_dist = ("uniform",-10, 10)
keep_cam_pose_proposal.min_terrain_distance = 1
-compose_scene.hero_boulders_chance = 1.0
+compose_nature.hero_boulders_chance = 1.0
terrain.elements.landtiles.LandTiles.y_tilt = 0.7
terrain.elements.landtiles.LandTiles.y_tilt_clip = 11
-camera_pose_proposal.override_loc = (0.678843,-30.5532, 11.7858)
-camera_pose_proposal.override_rot = (-110, 180, -178)
+camera_pose_proposal.location_sample = (0.678843,-30.5532, 11.7858)
+#camera_pose_proposal.override_rot = (-110, 180, -178)
+camera_pose_proposal.roll = 180
+camera_pose_proposal.yaw = -178
+camera_pose_proposal.pitch = -110
-compose_scene.tilted_river_enabled = True
+compose_nature.tilted_river_enabled = True
-assets.boulder.create_placeholder.boulder_scale = 3
+boulder.create_placeholder.boulder_scale = 3
LandTiles.land_process = None
-core.render.hide_water = True
+render.hide_water = True
compute_base_views.min_candidates_ratio = 1
walk_same_altitude.ignore_missed_rays = True
\ No newline at end of file
diff --git a/infinigen_examples/configs_nature/trailer_video.gin b/infinigen_examples/configs_nature/trailer_video.gin
new file mode 100644
index 000000000..52d7ec78a
--- /dev/null
+++ b/infinigen_examples/configs_nature/trailer_video.gin
@@ -0,0 +1,8 @@
+export.spherical = False # use OcMesher
+AnimPolicyRandomWalkLookaround.speed = ('uniform', 0.5, 1)
+AnimPolicyRandomWalkLookaround.step_speed_mult = 1
+AnimPolicyRandomWalkLookaround.yaw_sampler = ('uniform',-20, 20)
+AnimPolicyRandomWalkLookaround.step_range = ('clip_gaussian', 7, 5, 4, 10)
+AnimPolicyRandomWalkLookaround.rot_vars = (5, 0, 5)
+AnimPolicyRandomWalkLookaround.motion_dir_zoff = ('clip_gaussian', 0, 100, 0, 180)
+
diff --git a/infinigen_examples/generate_asset_demo.py b/infinigen_examples/generate_asset_demo.py
index e587a501f..e7374933c 100644
--- a/infinigen_examples/generate_asset_demo.py
+++ b/infinigen_examples/generate_asset_demo.py
@@ -117,11 +117,11 @@ def compose_scene(
# find a flat spot on the terrain to do the demo\
terrain = Terrain(scene_seed, surface.registry, task='coarse', on_the_fly_asset_folder=output_folder/"assets")
terrain_mesh = terrain.coarse_terrain()
- terrain_bvh = bvhtree.BVHTree.FromObject(terrain_mesh, bpy.context.evaluated_depsgraph_get())
+ scene_bvh = bvhtree.BVHTree.FromObject(terrain_mesh, bpy.context.evaluated_depsgraph_get())
if asset_factory is not None:
center = find_flat_location(
terrain_mesh,
- terrain_bvh,
+ scene_bvh,
rad=camera_circle_radius * 1.5,
alt=camera_altitude * 1.5
)
@@ -145,7 +145,7 @@ def compose_scene(
# snap all the locations the floor
for i, l in enumerate(locs):
- floorloc, *_ = terrain_bvh.ray_cast(Vector(l), Vector((0, 0, -1)))
+ floorloc, *_ = scene_bvh.ray_cast(Vector(l), Vector((0, 0, -1)))
if floorloc is None:
raise ValueError('Found a hole in the terain')
locs[i] = np.array(floorloc + Vector(asset_offset))
@@ -207,8 +207,8 @@ def main():
init.apply_gin_configs(
configs=args.configs,
overrides=args.overrides,
- configs_folder='infinigen_examples/configs',
- mandatory_folders=['infinigen_examples/configs/scene_types'],
+ configs_folder='infinigen_examples/configs_nature',
+ mandatory_folders=['infinigen_examples/configs_nature/scene_types'],
skip_unknown=True
)
diff --git a/infinigen_examples/generate_asset_parameters.py b/infinigen_examples/generate_asset_parameters.py
new file mode 100644
index 000000000..eb40bd499
--- /dev/null
+++ b/infinigen_examples/generate_asset_parameters.py
@@ -0,0 +1,329 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this
+# source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Alex Raistrick
+# - Karhan Kayan - add fire option
+
+import importlib
+import math
+import os
+import random
+import subprocess
+import traceback
+from collections.abc import Callable
+from itertools import product
+from pathlib import Path
+import logging
+
+from infinigen.assets.materials.woods import non_wood_tile, wood_tile
+from infinigen.assets.utils.object import new_cube, origin2lowest, center
+from infinigen.core.init import configure_cycles_devices
+from infinigen.core.surface import write_attr_data
+from infinigen.core.util.blender import deep_clone_obj
+from infinigen_examples.asset_parameters import parameters
+from infinigen_examples.generate_individual_assets import make_args, setup_camera
+from infinigen_examples.util.test_utils import load_txt_list
+
+logging.basicConfig(
+ format='[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] | %(message)s',
+ datefmt='%H:%M:%S', level=logging.WARNING
+)
+
+import bpy
+import gin
+import numpy as np
+from PIL import Image
+
+from infinigen.assets.fluid.fluid import set_obj_on_fire
+from infinigen.core.tagging import tag_system
+from infinigen.assets.lighting import (
+ sky_lighting, hdri_lighting, three_point_lighting, holdout_lighting,
+ CeilingLightFactory,
+)
+
+from infinigen.core import surface, init
+from infinigen.core.placement import factory
+from infinigen.core.util.camera import points_inview
+
+from infinigen.assets.utils.misc import subclasses
+from infinigen.assets.utils.decorate import read_base_co, read_co, read_normal
+
+from infinigen.core.util.math import FixedSeed
+# noinspection PyUnresolvedReferences
+from infinigen.core.util import blender as butil
+
+
+def build_scene_asset(args, factory_name, idx):
+ params = parameters[factory_name]['globals'].copy()
+ i = idx // parameters[factory_name]['repeat']
+ params.update(parameters[factory_name]['individuals'].copy()[i])
+ factory = parameters[factory_name]['factories'][i]
+ idx = parameters[factory_name]['indices'][i]
+ with FixedSeed(idx):
+ factory = factory(idx)
+ for k, v in params.items():
+ setattr(
+ factory, k,
+ v() if isinstance(v, Callable) and hasattr(v, '__name__') and v.__name__ == '' else v
+ )
+ with FixedSeed(idx):
+ if hasattr(factory, 'post_init'):
+ factory.post_init()
+ with FixedSeed(idx):
+ try:
+ if args.spawn_placeholder:
+ ph = factory.spawn_placeholder(idx, (0, 0, 0), (0, 0, 0))
+ asset = factory.spawn_asset(idx, placeholder=ph)
+ else:
+ asset = factory.spawn_asset(idx)
+ except Exception as e:
+ traceback.print_exc()
+ print(f'{factory}.spawn_asset({idx=}) FAILED!! {e}')
+ raise e
+ with FixedSeed(idx):
+ factory.finalize_assets(asset)
+ origin2lowest(asset, True)
+ bpy.context.view_layer.objects.active = asset
+ parent = asset
+ if asset.type == 'EMPTY':
+ meshes = [o for o in asset.children_recursive if o.type == 'MESH']
+ sizes = []
+ for m in meshes:
+ co = read_co(m)
+ sizes.append((np.amax(co, 0) - np.amin(co, 0)).sum())
+ i = np.argmax(np.array(sizes))
+ asset = meshes[i]
+ asset.location = -center(asset)
+ asset.location[-1] = 0
+ butil.apply_transform(asset, True)
+ if not args.no_mod:
+ if parent.animation_data is not None:
+ drivers = parent.animation_data.drivers.values()
+ for d in drivers:
+ parent.driver_remove(d.data_path)
+ if not args.no_ground:
+ plane = new_cube()
+ plane.scale = [2.5] * 3
+ co = read_co(asset)
+ plane.location = asset.location
+ plane.location[-1] += np.min(co[:, -1]) + 2.5
+ butil.apply_transform(plane, True)
+ plane_ = deep_clone_obj(plane)
+ plane_.location[-1] -= .1
+ plane_.scale = (1.5,) * 3
+ normal = read_normal(plane)
+ write_attr_data(plane, 'ground', normal[:, -1] < -.5, 'INT', 'FACE')
+ idx = parameters[factory_name]['scene_idx']
+ with FixedSeed(idx):
+ wood_tile.apply(plane, selection='ground')
+ non_wood_tile.apply(plane, selection='!ground', vertical=True)
+ factory = CeilingLightFactory(0)
+ factory.light_factory.params['Wattage'] = factory.light_factory.params['Wattage'] * 20
+ light = factory.spawn_asset(0)
+ light.location[-1] = np.min(co[:, -1]) + 5 - .5
+
+ return asset
+
+
+def build_scene(path, idx, factory_name, args):
+ scene = bpy.context.scene
+ scene.render.engine = 'CYCLES'
+ scene.render.resolution_x, scene.render.resolution_y = map(int, args.resolution.split('x'))
+ scene.cycles.samples = args.samples
+ configure_cycles_devices(True)
+ t = idx / (args.frame_end / args.cycles)
+ args.cam_angle = args.cam_angle[0], args.cam_angle[1], (np.abs(t - np.round(t)) * 2) * 180
+ if not args.fire:
+ bpy.context.scene.render.film_transparent = args.film_transparent
+ bpy.context.scene.world.node_tree.nodes['Background'].inputs[0].default_value[-1] = 0
+
+ if idx % parameters[factory_name]['repeat'] == 0:
+ butil.clear_scene()
+ camera, center = setup_camera(args)
+ asset = build_scene_asset(args, factory_name, idx)
+
+ with FixedSeed(args.lighting):
+ if args.hdri:
+ hdri_lighting.add_lighting()
+ elif args.three_point:
+ holdout_lighting.add_lighting()
+ three_point_lighting.add_lighting(asset)
+ else:
+ sky_lighting.add_lighting(camera)
+ nodes = bpy.data.worlds['World'].node_tree.nodes
+ sky_texture = [n for n in nodes if n.name.startswith('Sky Texture')][-1]
+ sky_texture.sun_elevation = np.deg2rad(args.elevation)
+ sky_texture.sun_rotation = np.pi * .75
+
+ else:
+ camera, center = setup_camera(args)
+ asset = list(o for o in bpy.data.objects if 'Factory' in o.name and o.parent is None)[0]
+
+ if args.scale_reference:
+ bpy.ops.mesh.primitive_cylinder_add(radius=0.3, depth=1.8, location=(4.9, 4.9, 1.8 / 2))
+
+ if args.cam_center > 0 and asset:
+ co = read_base_co(asset) + asset.location
+ center.location = (np.amin(co, 0) + np.amax(co, 0)) / 2
+ center.location[-1] += args.cam_zoff
+
+ if args.cam_dist <= 0 and asset:
+ if 'Factory' in factory_name:
+ adjust_cam_distance(asset, camera, args.margin)
+ else:
+ adjust_cam_distance(asset, camera, args.margin, .75)
+
+ cam_info_ng = bpy.data.node_groups.get('nodegroup_active_cam_info')
+ if cam_info_ng is not None:
+ cam_info_ng.nodes['Object Info'].inputs['Object'].default_value = camera
+
+ if args.save_blend:
+ (path / 'scenes').mkdir(exist_ok=True)
+ butil.save_blend(f"{path}/scenes/scene_{idx:03d}.blend", autopack=True)
+ tag_system.save_tag(f"{path}/MaskTag.json")
+
+ if args.fire:
+ bpy.data.worlds['World'].node_tree.nodes["Background.001"].inputs[1].default_value = 0.04
+ bpy.context.scene.view_settings.exposure = -2
+
+ if args.render == 'image':
+ (path / 'images').mkdir(exist_ok=True)
+ imgpath = path / f"images/image_{idx:03d}.png"
+ scene.render.filepath = str(imgpath)
+ bpy.ops.render.render(write_still=True)
+ elif args.render == 'video':
+ bpy.context.scene.frame_end = args.frame_end
+ t = f"frame / {args.frame_end / args.cycles}"
+ parent(asset).driver_add('rotation_euler')[-1].driver.expression = f'(abs({t}-round({t}))*2-.5)*{np.pi}'
+ (path / 'frames' / f'scene_{idx:03d}').mkdir(parents=True, exist_ok=True)
+ imgpath = path / f"frames/scene_{idx:03d}/frame_###.png"
+ scene.render.filepath = str(imgpath)
+ bpy.ops.render.render(animation=True)
+
+
+def parent(obj):
+ return obj if obj.parent is None else obj.parent
+
+
+def adjust_cam_distance(asset, camera, margin, percent=.999):
+ co = read_base_co(asset) * asset.scale
+ co += asset.location
+ lowest = np.amin(co, 0)
+ highest = np.amax(co, 0)
+ interp = np.linspace(lowest, highest, 11)
+ bbox = np.array(list(product(*zip(*interp))))
+ for cam_dist in np.exp(np.linspace(-1., 5.5, 500)):
+ camera.location[1] = -cam_dist
+ bpy.context.view_layer.update()
+ inview = points_inview(bbox, camera)
+ if inview.sum() / inview.size >= percent:
+ camera.location[1] *= 1 + margin
+ bpy.context.view_layer.update()
+ break
+ else:
+ camera.location[1] = -6
+
+
+def make_grid(args, path, n):
+ files = []
+ for filename in sorted(os.listdir(f'{path}/images')):
+ if filename.endswith('.png'):
+ files.append(f'{path}/images/{filename}')
+ files = files[:n]
+ if len(files) == 0:
+ print('No images found')
+ return
+ with Image.open(files[0]) as i:
+ x, y = i.size
+ for i, name in enumerate([path.stem, f'{path.stem}_']):
+ if args.zoom:
+ img = Image.new('RGBA', (2 * x, y))
+ sz = int(np.floor(np.sqrt(n - .9)))
+ if i > 0:
+ random.shuffle(files)
+ with Image.open(files[0]) as i:
+ img.paste(i, (0, 0))
+ for idx in range(sz ** 2):
+ with Image.open(files[min(idx + 1, len(files) - 1)]) as i:
+ img.paste(i.resize((x // sz, y // sz)), (x + (idx % sz) * (x // sz), idx // sz * (y // sz)))
+ img.save(f'{path}/{name}.png')
+ else:
+ sz_x = list(sorted(range(1, n + 1), key=lambda x: abs(math.ceil(n / x) / x - args.best_ratio)))[0]
+ sz_y = math.ceil(n / sz_x)
+ img = Image.new('RGBA', (sz_x * x, sz_y * y))
+ if i > 0:
+ random.shuffle(files)
+ for idx, file in enumerate(files):
+ with Image.open(file) as i:
+ img.paste(i, (idx % sz_x * x, idx // sz_x * y))
+ img.save(f'{path}/{name}.png')
+
+
+def main(args):
+ bpy.context.window.workspace = bpy.data.workspaces['Geometry Nodes']
+
+ init.apply_gin_configs('infinigen_examples/configs_indoor', skip_unknown=True)
+ surface.registry.initialize_from_gin()
+
+ extras = '[%(filename)s:%(lineno)d] ' if args.loglevel == logging.DEBUG else ''
+ logging.basicConfig(
+ format=f'[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] {extras}| %(message)s',
+ level=args.loglevel, datefmt='%H:%M:%S'
+ )
+ logging.getLogger("infinigen").setLevel(args.loglevel)
+
+ if '.txt' in args.factories[0]:
+ name = args.factories[0].split('.')[-2].split('/')[-1]
+ else:
+ name = '_'.join(args.factories)
+ path = Path(os.getcwd()) / 'outputs' / name
+ path.mkdir(exist_ok=True)
+
+ factories = list(args.factories)
+ if 'ALL_ASSETS' in factories:
+ factories += [f.__name__ for f in subclasses(factory.AssetFactory)]
+ factories.remove('ALL_ASSETS')
+ if 'ALL_SCATTERS' in factories:
+ factories += [f.stem for f in Path('surfaces/scatters').iterdir()]
+ factories.remove('ALL_SCATTERS')
+ if 'ALL_MATERIALS' in factories:
+ factories += [f.stem for f in Path('infinigen/assets/materials').iterdir()]
+ factories.remove('ALL_MATERIALS')
+ if '.txt' in factories[0]:
+ factories = [f.split('.')[-1] for f in load_txt_list(factories[0], skip_sharp=False)]
+
+ for fac in factories:
+ fac_path = path / fac
+ if fac_path.exists() and args.skip_existing:
+ continue
+ fac_path.mkdir(exist_ok=True)
+ n_images = args.n_images
+ if not args.postprocessing_only:
+ for idx in range(args.n_images):
+ try:
+ build_scene(fac_path, idx, fac, args)
+ except Exception as e:
+ print(e)
+ continue
+ if args.render == 'image':
+ make_grid(args, fac_path, n_images)
+ if args.render == 'video':
+ (fac_path / 'videos').mkdir(exist_ok=True)
+ for i in range(n_images):
+ subprocess.run(
+ f'ffmpeg -y -r 24 -pattern_type glob -i "{fac_path}/frames/scene_{i:03d}/frame*.png" '
+ f'{fac_path}/videos/video_{i:03d}.mp4', shell=True
+ )
+
+
+if __name__ == '__main__':
+ args = make_args()
+ args.no_mod = args.no_mod or args.fire
+ args.film_transparent = args.film_transparent and not args.hdri
+ args.n_images = len(parameters[args.factories[0]]['factories']) * parameters[args.factories[0]]['repeat']
+ with FixedSeed(1):
+ main(args)
diff --git a/infinigen_examples/generate_individual_assets.py b/infinigen_examples/generate_individual_assets.py
index b32c282f1..6f8e8f9f0 100644
--- a/infinigen_examples/generate_individual_assets.py
+++ b/infinigen_examples/generate_individual_assets.py
@@ -1,8 +1,9 @@
# Copyright (c) Princeton University.
-# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this
# source tree.
-# Authors:
+# Authors:
# - Lingjie Mei
# - Alex Raistrick
# - Karhan Kayan - add fire option
@@ -11,20 +12,19 @@
import importlib
import math
import os
+import random
import re
import subprocess
-import sys
import traceback
from itertools import product
from pathlib import Path
import logging
from multiprocessing import Pool
-logging.basicConfig(
- format='[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] | %(message)s',
- datefmt='%H:%M:%S',
- level=logging.WARNING
-)
+from infinigen.core.init import configure_cycles_devices
+
+logging.basicConfig(format='[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] | %(message)s',
+ datefmt='%H:%M:%S', level=logging.WARNING)
import bpy
import gin
@@ -34,43 +34,26 @@
import submitit
from infinigen.assets.fluid.fluid import set_obj_on_fire
-from infinigen.assets.utils.decorate import assign_material, read_base_co
-from infinigen.assets.utils.tag import tag_object, tag_nodegroup, tag_system
-from infinigen.assets.lighting import sky_lighting
+from infinigen.core.tagging import tag_system
+from infinigen.assets.lighting import sky_lighting, hdri_lighting, three_point_lighting, holdout_lighting
from infinigen.core import surface, init
-from infinigen.core.placement import density, factory
-from infinigen.infinigen_gpl.extras.enable_gpu import enable_gpu
+from infinigen.core.placement import density, factory
+from infinigen.core.util.camera import points_inview
+
+from infinigen.assets.utils.misc import assign_material, subclasses
+# from infinigen.core.rendering.render import enable_gpu
+from infinigen.assets.utils.decorate import read_base_co, read_co
+
from infinigen.core.util.math import FixedSeed
-from infinigen.core.util.camera import get_3x4_P_matrix_from_blender
-from infinigen.core.util.logging import Suppress
+# noinspection PyUnresolvedReferences
from infinigen.core.util import blender as butil
from infinigen.tools import export
-def load_txt_list(path, skip_sharp=False):
- res = (Path(__file__).parent/path).read_text().splitlines()
- res = [
- f.lstrip('#').lstrip(' ')
- for f in res if
- len(f) > 0 and not '#' in f
- ]
- print(res)
- return res
-
-def load_txt_list(path, skip_sharp=False):
- res = (Path(__file__).parent/path).read_text().splitlines()
- res = [
- f.lstrip('#').lstrip(' ')
- for f in res if
- len(f) > 0 and not '#' in f
- ]
- print(res)
- return res
-
-from . import generate_nature # to load most/all factory.AssetFactory subclasses
-
-def build_scene_asset(factory_name, idx):
+from infinigen_examples.util.test_utils import load_txt_list
+
+def build_scene_asset(args, factory_name, idx):
factory = None
for subdir in os.listdir('infinigen/assets'):
with gin.unlock_config():
@@ -83,7 +66,11 @@ def build_scene_asset(factory_name, idx):
with FixedSeed(idx):
factory = factory(idx)
try:
- asset = factory.spawn_asset(idx)
+ if args.spawn_placeholder:
+ ph = factory.spawn_placeholder(idx, (0, 0, 0), (0, 0, 0))
+ asset = factory.spawn_asset(idx, placeholder=ph)
+ else:
+ asset = factory.spawn_asset(idx)
except Exception as e:
traceback.print_exc()
print(f'{factory}.spawn_asset({idx=}) FAILED!! {e}')
@@ -103,27 +90,28 @@ def build_scene_asset(factory_name, idx):
meshes = [o for o in asset.children_recursive if o.type == 'MESH']
sizes = []
for m in meshes:
- co = read_base_co(m)
+ co = read_co(m)
sizes.append((np.amax(co, 0) - np.amin(co, 0)).sum())
i = np.argmax(np.array(sizes))
asset = meshes[i]
- if not args.fire:
+ if not args.no_mod:
if parent.animation_data is not None:
drivers = parent.animation_data.drivers.values()
for d in drivers:
parent.driver_remove(d.data_path)
- co = read_base_co(asset)
+ co = read_co(asset)
x_min, x_max = np.amin(co, 0), np.amax(co, 0)
parent.location = -(x_min[0] + x_max[0]) / 2, -(x_min[1] + x_max[1]) / 2, 0
butil.apply_transform(parent, loc=True)
- bpy.ops.mesh.primitive_grid_add(size=5, x_subdivisions=400, y_subdivisions=400)
- plane = bpy.context.active_object
- plane.location[-1] = x_min[-1]
- plane.is_shadow_catcher = True
- material = bpy.data.materials.new('plane')
- material.use_nodes = True
- material.node_tree.nodes['Principled BSDF'].inputs[0].default_value = .015, .009, .003, 1
- assign_material(plane, material)
+ if not args.no_ground:
+ bpy.ops.mesh.primitive_grid_add(size=5, x_subdivisions=400, y_subdivisions=400)
+ plane = bpy.context.active_object
+ plane.location[-1] = x_min[-1]
+ plane.is_shadow_catcher = True
+ material = bpy.data.materials.new('plane')
+ material.use_nodes = True
+ material.node_tree.nodes['Principled BSDF'].inputs[0].default_value = .015, .009, .003, 1
+ assign_material(plane, material)
return asset
@@ -143,15 +131,32 @@ def build_scene_surface(factory_name, idx):
material.use_nodes = True
material.node_tree.nodes['Principled BSDF'].inputs[0].default_value = .015, .009, .003, 1
assign_material(plane, material)
-
+ if type(scatter) is type:
+ scatter = scatter(idx)
scatter.apply(plane, selection=density.placement_mask(.15, .45))
asset = plane
except ModuleNotFoundError:
try:
with gin.unlock_config():
- template = importlib.import_module(f'infinigen.assets.materials.{factory_name}')
- bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=9)
- asset = bpy.context.active_object
+ try:
+ template = importlib.import_module(f'infinigen.assets.materials.{factory_name}')
+ except:
+ for subdir in os.listdir('infinigen/assets/materials'):
+ with gin.unlock_config():
+ module = importlib.import_module(
+ f'infinigen.assets.materials.{subdir.split(".")[0]}')
+ if hasattr(module, factory_name):
+ template = getattr(module, factory_name)
+ break
+ else:
+ raise Exception(f'{factory_name} not Found.')
+ if hasattr(template, 'make_sphere'):
+ asset = template.make_sphere()
+ else:
+ bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=9)
+ asset = bpy.context.active_object
+ if type(template) is type:
+ template = template(idx)
template.apply(asset)
except ModuleNotFoundError:
raise Exception(f'{factory_name} not Found.')
@@ -169,11 +174,9 @@ def build_and_save_asset(payload: dict):
if args.seed > 0:
idx = args.seed
- if args.gpu:
- enable_gpu()
-
path = args.output_folder / factory_name
- if path and args.skip_existing:
+ if (path / f"images/image_{idx:03d}.png").exists() and args.skip_existing:
+ print(f'Skipping {path}')
return
path.mkdir(exist_ok=True)
@@ -182,24 +185,31 @@ def build_and_save_asset(payload: dict):
scene.render.resolution_x, scene.render.resolution_y = map(int, args.resolution.split('x'))
scene.cycles.samples = args.samples
butil.clear_scene()
+ configure_cycles_devices()
if not args.fire:
bpy.context.scene.render.film_transparent = args.film_transparent
bpy.context.scene.world.node_tree.nodes['Background'].inputs[0].default_value[-1] = 0
camera, center = setup_camera(args)
- with FixedSeed(args.lighting):
- sky_lighting.add_lighting(camera)
- nodes = bpy.data.worlds['World'].node_tree.nodes
- sky_texture = [n for n in nodes if n.name.startswith('Sky Texture')][-1]
- sky_texture.sun_elevation = np.deg2rad(args.elevation)
- sky_texture.sun_rotation = np.pi * .75
-
if 'Factory' in factory_name:
- asset = build_scene_asset(factory_name, idx)
+ asset = build_scene_asset(args, factory_name, idx)
else:
asset = build_scene_surface(factory_name, idx)
+ with FixedSeed(args.lighting + idx):
+ if args.hdri:
+ hdri_lighting.add_lighting()
+ elif args.three_point:
+ holdout_lighting.add_lighting()
+ three_point_lighting.add_lighting(asset)
+ else:
+ sky_lighting.add_lighting(camera)
+ nodes = bpy.data.worlds['World'].node_tree.nodes
+ sky_texture = [n for n in nodes if n.name.startswith('Sky Texture')][-1]
+ sky_texture.sun_elevation = np.deg2rad(args.elevation)
+ sky_texture.sun_rotation = np.pi * .75
+
if args.scale_reference:
bpy.ops.mesh.primitive_cylinder_add(radius=0.3, depth=1.8, location=(4.9, 4.9, 1.8 / 2))
@@ -209,7 +219,10 @@ def build_and_save_asset(payload: dict):
center.location[-1] += args.cam_zoff
if args.cam_dist <= 0 and asset:
- adjust_cam_distance(asset, camera, args.margin)
+ if 'Factory' in factory_name:
+ adjust_cam_distance(asset, camera, args.margin)
+ else:
+ adjust_cam_distance(asset, camera, args.margin, .75)
cam_info_ng = bpy.data.node_groups.get('nodegroup_active_cam_info')
if cam_info_ng is not None:
@@ -217,7 +230,7 @@ def build_and_save_asset(payload: dict):
if args.save_blend:
(path / 'scenes').mkdir(exist_ok=True)
- bpy.ops.wm.save_as_mainfile(filepath=f"{path}/scenes/scene_{idx:03d}.blend", filter_backup=True)
+ butil.save_blend(f"{path}/scenes/scene_{idx:03d}.blend", autopack=True)
tag_system.save_tag(f"{path}/MaskTag.json")
if args.fire:
@@ -251,35 +264,21 @@ def build_and_save_asset(payload: dict):
image_res=args.export_texture_res
)
- if args.export is not None:
- export_path = path/'export'/f'export_{idx:03d}'
- export_path.mkdir(exist_ok=True, parents=True)
- export.export_curr_scene(
- export_path,
- format=args.export,
- image_res=args.export_texture_res
- )
-
-
def parent(obj):
return obj if obj.parent is None else obj.parent
-
-def adjust_cam_distance(asset, camera, margin):
- co = read_base_co(asset)
+def adjust_cam_distance(asset, camera, margin, percent=.999):
+ co = read_base_co(asset) * asset.scale
co += asset.location
lowest = np.amin(co, 0)
highest = np.amax(co, 0)
- bbox = np.array(list(product(*zip(lowest, highest))))
- render = bpy.context.scene.render
- for cam_dist in np.exp(np.linspace(-.5, 3.5, 100)):
+ interp = np.linspace(lowest, highest, 11)
+ bbox = np.array(list(product(*zip(*interp))))
+ for cam_dist in np.exp(np.linspace(-1., 5.5, 500)):
camera.location[1] = -cam_dist
bpy.context.view_layer.update()
- proj = np.array(get_3x4_P_matrix_from_blender(camera)[0])
- x, y, z = proj @ np.concatenate([bbox, np.ones((len(bbox), 1))], -1).T
- inview = (np.all(z > 0) and np.all(x >= 0) and np.all(z > 0) and np.all(
- x / z < render.resolution_x) and np.all(y / z < render.resolution_y))
- if inview:
+ inview = points_inview(bbox, camera)
+ if inview.sum() / inview.size >= percent:
camera.location[1] *= 1 + margin
bpy.context.view_layer.update()
break
@@ -298,13 +297,29 @@ def make_grid(args, path, n):
return
with Image.open(files[0]) as i:
x, y = i.size
- sz_x = list(sorted(range(1, n + 1), key=lambda x: abs(math.ceil(n / x) / x - args.best_ratio)))[0]
- sz_y = math.ceil(n / sz_x)
- img = Image.new('RGBA', (sz_x * x, sz_y * y))
- for idx, file in enumerate(files):
- with Image.open(file) as i:
- img.paste(i, (idx % sz_x * x, idx // sz_x * y))
- img.save(f'{path}/grid.png')
+ for i, name in enumerate([path.stem, f'{path.stem}_']):
+ if args.zoom:
+ img = Image.new('RGBA', (2 * x, y))
+ sz = int(np.floor(np.sqrt(n - .9)))
+ if i > 0:
+ random.shuffle(files)
+ with Image.open(files[0]) as i:
+ img.paste(i, (0, 0))
+ for idx in range(sz ** 2):
+ with Image.open(files[min(idx + 1, len(files) - 1)]) as i:
+ img.paste(i.resize((x // sz, y // sz)), (x + (idx % sz) * (x // sz), idx // sz * (y // sz)))
+ img.save(f'{path}/{name}.png')
+ else:
+ sz_x = list(sorted(range(1, n + 1), key=lambda x: abs(math.ceil(n / x) / x - args.best_ratio)))[0]
+ sz_y = math.ceil(n / sz_x)
+ img = Image.new('RGBA', (sz_x * x, sz_y * y))
+ if i > 0:
+ random.shuffle(files)
+ for idx, file in enumerate(files):
+ with Image.open(file) as i:
+ img.paste(i, (idx % sz_x * x, idx // sz_x * y))
+ img.save(f'{path}/{name}.png')
+ print(f'{path}/{name}.png generated')
def setup_camera(args):
@@ -350,24 +365,34 @@ def mapfunc(f, its, args):
jobs = executor.map_array(f, its)
for j in jobs:
print(f'Job finished {j.wait()}')
-
def main(args):
bpy.context.window.workspace = bpy.data.workspaces['Geometry Nodes']
-
- init.apply_gin_configs('infinigen_examples/configs')
+
+ init.apply_gin_configs('infinigen_examples/configs_indoor', skip_unknown=True)
surface.registry.initialize_from_gin()
init.configure_blender()
+ if args.gpu:
+ init.configure_render_cycles()
+
extras = '[%(filename)s:%(lineno)d] ' if args.loglevel == logging.DEBUG else ''
- logging.basicConfig(
- format=f'[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] {extras}| %(message)s',
- level=args.loglevel,
- datefmt='%H:%M:%S'
- )
+ logging.basicConfig(format=f'[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] {extras}| %(message)s',
+ level=args.loglevel, datefmt='%H:%M:%S')
logging.getLogger("infinigen").setLevel(args.loglevel)
+ if '.txt' in args.factories[0]:
+ name = args.factories[0].split('.')[-2].split('/')[-1]
+ else:
+ name = '_'.join(args.factories)
+
+ if args.output_folder is None:
+ args.output_folder = Path(os.getcwd()) / 'outputs'
+
+ path = Path(args.output_folder) / name
+ path.mkdir(exist_ok=True)
+
factories = list(args.factories)
if 'ALL_ASSETS' in factories:
factories += [f.__name__ for f in subclasses(factory.AssetFactory)]
@@ -378,8 +403,9 @@ def main(args):
if 'ALL_MATERIALS' in factories:
factories += [f.stem for f in Path('infinigen/assets/materials').iterdir()]
factories.remove('ALL_MATERIALS')
-
- args.output_folder.mkdir(exist_ok=True)
+ has_txt = '.txt' in factories[0]
+ if has_txt:
+ factories = [f.split('.')[-1] for f in load_txt_list(factories[0], skip_sharp=False)]
if not args.postprocessing_only:
for fac in factories:
@@ -389,12 +415,21 @@ def main(args):
]
mapfunc(build_and_save_asset, targets, args)
- for fac in factories:
+ for j, fac in enumerate(factories):
fac_path = args.output_folder/fac
- assert fac_path.exists()
- if args.render == 'image':
+ assert fac_path.exists(); f'{fac_path} does not exist'
+ if has_txt:
+ for i in range(args.n_images):
+ img_path = fac_path / 'images' / f'image_{i:03d}.png'
+ if img_path.exists():
+ subprocess.run(
+ f'cp -f {img_path} {path}/{fac}_{i:03d}.png', shell=True
+ )
+ else:
+ print(f'{img_path} does not exist')
+ elif args.render == 'image':
make_grid(args, fac_path, args.n_images)
- if args.render == 'video':
+ elif args.render == 'video':
(fac_path / 'videos').mkdir(exist_ok=True)
for i in range(args.n_images):
subprocess.run(
@@ -402,48 +437,62 @@ def main(args):
f'{fac_path}/videos/video_{i:03d}.mp4', shell=True)
+
def snake_case(s):
return '_'.join(
re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', s.replace('-', ' '))).split()).lower()
def make_args():
parser = argparse.ArgumentParser()
- parser.add_argument('--output_folder', type=Path)
+ parser.add_argument('-o', '--output_folder', type=Path, default=None)
parser.add_argument('-f', '--factories', default=[], nargs='+',
help="List factories/surface scatters/surface materials you want to render")
- parser.add_argument('-n', '--n_images', default=4, type=int, help="Number of scenes to render")
- parser.add_argument("-m", '--margin', default=.1,
+ parser.add_argument('-n', '--n_images', default=1, type=int, help="Number of scenes to render")
+ parser.add_argument("-m", '--margin', default=.01,
help="Margin between the asset the boundary of the image when automatically adjusting "
"the camera")
parser.add_argument('-R', '--resolution', default='1024x1024', type=str,
help="Image resolution widthxheight")
parser.add_argument('-p', '--samples', default=200, type=int, help="Blender cycles samples")
parser.add_argument('-l', '--lighting', default=0, type=int, help="Lighting seed")
- parser.add_argument('-o', '--cam_zoff', '--z_offset', type=float, default=.0,
+ parser.add_argument('-Z', '--cam_zoff', '--z_offset', type=float, default=.0,
help="Additional offset on Z axis for camera look-at positions")
parser.add_argument('-s', '--save_blend', action='store_true', help="Whether to save .blend file")
parser.add_argument('-e', '--elevation', default=60, type=float, help="Elevation of the sun")
parser.add_argument('--cam_dist', default=0, type=float,
- help="Distance from the camera to the look-at position")
- parser.add_argument('-a', '--cam_angle', default=(-30, 0, 0), type=float, nargs='+',
- help="Camera rotation in XYZ")
+ help="Distance from the camera to the look-at position"
+ )
+ parser.add_argument(
+ '-a', '--cam_angle', default=(-30, 0, 45), type=float, nargs='+',
+ help="Camera rotation in XYZ"
+ )
+ parser.add_argument('-O', '--offset', default=(0, 0, 0), type=float, nargs='+', help='asset location')
parser.add_argument('-c', '--cam_center', default=1, type=int, help="Camera rotation in XYZ")
- parser.add_argument('-r', '--render', default='image', type=str, choices=['image', 'video', 'none'],
+ parser.add_argument(
+ '-r', '--render', default='image', type=str, choices=['image', 'video', 'none'],
help="Whether to render the scene in images or video")
parser.add_argument('-b', '--best_ratio', default=9 / 16, type=float,
help="Best aspect ratio for compiling the images into asset grid")
- parser.add_argument('-F', '--fire', action = 'store_true')
- parser.add_argument('-I', '--fire_res', default = 100, type = int)
- parser.add_argument('-U', '--fire_duration', default = 30, type = int)
+ parser.add_argument('-F', '--fire', action='store_true')
+ parser.add_argument('-I', '--fire_res', default=100, type=int)
+ parser.add_argument('-U', '--fire_duration', default=30, type=int)
parser.add_argument('-t', '--film_transparent', default=1, type=int,
help="Whether the background is transparent")
parser.add_argument('-E', '--frame_end', type=int, default=120, help="End of frame in videos")
+ parser.add_argument('-g', '--gpu', action='store_true', help="Whether to use gpu in rendering")
parser.add_argument('-C', '--cycles', type=float, default=1, help="render video cycles")
parser.add_argument('-A', '--scale_reference', action='store_true', help="Add the scale reference")
parser.add_argument('-S', '--skip_existing', action='store_true', help="Skip existing scenes and renders")
parser.add_argument('-P', '--postprocessing_only', action='store_true', help="Only run postprocessing")
parser.add_argument('-D', '--seed', type=int, default=-1, help="Run a specific seed.")
- parser.add_argument('-d', '--debug', action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
+ parser.add_argument('-N', '--no-mod', action='store_true', help="No modification")
+ parser.add_argument('-d', '--debug', action="store_const", dest="loglevel", const=logging.DEBUG,
+ default=logging.INFO)
+ parser.add_argument('-H', '--hdri', action='store_true', help="add_hdri")
+ parser.add_argument('-T', '--three_point', action='store_true', help="add three-point lighting")
+ parser.add_argument('-G', '--no_ground', action='store_true', help="no ground")
+ parser.add_argument('-W', '--spawn_placeholder', action='store_true', help="spawn placeholder")
+ parser.add_argument('-z', '--zoom', action='store_true', help="zoom first figure")
parser.add_argument('--n_workers', type=int, default=1)
parser.add_argument('--slurm', action='store_true')
@@ -453,7 +502,10 @@ def make_args():
return init.parse_args_blender(parser)
+
if __name__ == '__main__':
args = make_args()
+ args.no_mod = args.no_mod or args.fire
+ args.film_transparent = args.film_transparent and not args.hdri
with FixedSeed(1):
main(args)
diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py
new file mode 100644
index 000000000..80fac6cd8
--- /dev/null
+++ b/infinigen_examples/generate_indoors.py
@@ -0,0 +1,426 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+import argparse
+from pathlib import Path
+import logging
+from time import time
+from numpy import deg2rad
+import pprint
+import copy
+
+logging.basicConfig(
+ format='[%(asctime)s.%(msecs)03d] [%(module)s] [%(levelname)s] | %(message)s',
+ datefmt='%H:%M:%S',
+ level=logging.INFO
+)
+
+import bpy
+from mathutils import Vector
+import gin
+import numpy as np
+import trimesh
+from numpy import deg2rad
+
+from infinigen.assets import (lighting)
+from infinigen.assets.wall_decorations.skirting_board import make_skirting_board
+from infinigen.assets.utils.decorate import read_co
+from infinigen.terrain import Terrain
+from infinigen.assets.materials import invisible_to_camera
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ example_solver,
+ checks,
+ usage_lookup
+)
+
+from infinigen.assets import (
+ fluid,
+ cactus,
+ cactus,
+ trees,
+ monocot,
+ rocks,
+ underwater,
+ creatures,
+ lighting,
+ weather
+)
+from infinigen.assets.scatters import grass, pebbles
+from infinigen.assets.utils.decorate import read_co
+
+from infinigen.core.placement import density, camera as cam_util, split_in_view
+
+from infinigen_examples.indoor_constraint_examples import home_constraints
+from infinigen.core import (
+ execute_tasks,
+ surface,
+ init,
+ placement,
+ tags as t,
+ tagging
+)
+from infinigen.core.util import (blender as butil, pipeline)
+
+from infinigen.core.constraints.example_solver.room import decorate as room_dec, constants
+from infinigen.core.constraints.example_solver import state_def, greedy, populate, Solver
+
+from infinigen.core.constraints.example_solver.room.constants import WALL_HEIGHT
+from infinigen.core.util.camera import points_inview
+
+from infinigen_examples.generate_nature import compose_nature # so gin can find it
+from infinigen_examples.util import constraint_util as cu
+from infinigen_examples.util.generate_indoors_util import (
+ create_outdoor_backdrop,
+ place_cam_overhead,
+ overhead_view,
+ hide_other_rooms,
+ restrict_solving,
+ apply_greedy_restriction
+)
+
+logger = logging.getLogger(__name__)
+
+def default_greedy_stages():
+
+ """Returns descriptions of what will be covered by each greedy stage of the solver.
+
+ Any domain containing one or more VariableTags is greedy: it produces many separate domains,
+ one for each possible assignment of the unresolved variables.
+ """
+
+ on_floor = cl.StableAgainst({}, cu.floortags)
+ on_wall = cl.StableAgainst({}, cu.walltags)
+ on_ceiling = cl.StableAgainst({}, cu.ceilingtags)
+ side = cl.StableAgainst({}, cu.side)
+
+ all_room = r.Domain({t.Semantics.Room, -t.Semantics.Object})
+ all_obj = r.Domain({t.Semantics.Object, -t.Semantics.Room})
+
+ all_obj_in_room = all_obj.with_relation(cl.AnyRelation(), all_room.with_tags(cu.variable_room))
+ primary = all_obj_in_room.with_relation(-cl.AnyRelation(), all_obj)
+
+ greedy_stages = {}
+
+ greedy_stages['rooms'] = all_room
+
+ greedy_stages['on_floor'] = primary.with_relation(on_floor, all_room)
+ greedy_stages['on_wall'] = (
+ primary.with_relation(-on_floor, all_room)
+ .with_relation(-on_ceiling, all_room)
+ .with_relation(on_wall, all_room)
+ )
+ greedy_stages['on_ceiling'] = (
+ primary.with_relation(-on_floor, all_room)
+ .with_relation(on_ceiling, all_room)
+ .with_relation(-on_wall, all_room)
+ )
+
+ secondary = all_obj.with_relation(cl.AnyRelation(), primary.with_tags(cu.variable_obj))
+
+ greedy_stages['side_obj'] = secondary.with_relation(side, all_obj)
+ nonside = secondary.with_relation(-side, all_obj)
+
+ greedy_stages['obj_ontop_obj'] = nonside.with_relation(cu.ontop, all_obj).with_relation(-cu.on, all_obj)
+ greedy_stages['obj_on_support'] = nonside.with_relation(cu.on, all_obj).with_relation(-cu.ontop, all_obj)
+
+
+ return greedy_stages
+
+
+all_vars = [cu.variable_room, cu.variable_obj]
+
+@gin.configurable
+def compose_indoors(output_folder: Path, scene_seed: int, **overrides):
+ p = pipeline.RandomStageExecutor(scene_seed, output_folder, overrides)
+
+ logger.debug(overrides)
+
+ def add_coarse_terrain():
+ terrain = Terrain(
+ scene_seed,
+ surface.registry,
+ task='coarse',
+ on_the_fly_asset_folder=output_folder / "assets"
+ )
+ terrain_mesh = terrain.coarse_terrain()
+ # placement.density.set_tag_dict(terrain.tag_dict)
+ return terrain, terrain_mesh
+
+ terrain, terrain_mesh = p.run_stage('terrain', add_coarse_terrain, use_chance=False, default=(None, None))
+ p.run_stage('sky_lighting', lighting.sky_lighting.add_lighting, use_chance=False)
+
+ consgraph = home_constraints()
+ stages = default_greedy_stages()
+ checks.check_all(consgraph, stages, all_vars)
+
+ stages, consgraph, limits = restrict_solving(stages, consgraph)
+
+ if overrides.get('restrict_single_supported_roomtype', False):
+ restrict_parent_rooms = {
+ np.random.choice([
+
+ # Only these roomtypes have constraints written in home_constraints.
+ # Others will be empty-ish besides maybe storage and plants
+ # TODO: add constraints to home_constraints for garages, offices, balconies, etc
+
+ t.Semantics.Bedroom,
+ t.Semantics.LivingRoom,
+ t.Semantics.Kitchen,
+ t.Semantics.Bathroom,
+ t.Semantics.DiningRoom
+ ])
+ }
+ logger.info(f'Restricting to {restrict_parent_rooms}')
+ apply_greedy_restriction(stages, restrict_parent_rooms, cu.variable_room)
+
+ solver = Solver(output_folder=output_folder)
+ def solve_rooms():
+ return solver.solve_rooms(scene_seed, consgraph, stages['rooms'])
+ state: state_def.State = p.run_stage('solve_rooms', solve_rooms, use_chance=False)
+
+ def solve_large():
+ assignments = greedy.iterate_assignments(
+ stages['on_floor'], state, all_vars, limits, nonempty=True
+ )
+ for i, vars in enumerate(assignments):
+ solver.solve_objects(
+ consgraph,
+ stages['on_floor'],
+ var_assignments=vars,
+ n_steps=overrides['solve_steps_large'],
+ desc=f"on_floor_{i}",
+ abort_unsatisfied=overrides.get('abort_unsatisfied_large', False)
+ )
+ return solver.state
+ state = p.run_stage('solve_large', solve_large, use_chance=False, default=state)
+
+ solved_rooms = [
+ state.objs[assignment[cu.variable_room]].obj
+ for assignment in greedy.iterate_assignments(
+ stages['on_floor'], state, [cu.variable_room], limits
+ )
+ ]
+ solved_bound_points = np.concatenate([butil.bounds(r) for r in solved_rooms])
+ solved_bbox = (np.min(solved_bound_points, axis=0), np.max(solved_bound_points, axis=0))
+
+ house_bbox = np.concatenate([butil.bounds(obj) for obj in solver.get_bpy_objects(r.Domain({t.Semantics.Room}))])
+ house_bbox = (np.min(house_bbox, axis=0), np.max(house_bbox, axis=0))
+
+ camera_rigs = placement.camera.spawn_camera_rigs()
+
+ def pose_cameras():
+
+ nonroom_objs = [
+ o.obj for o in state.objs.values() if t.Semantics.Room not in o.tags
+ ]
+ scene_objs = solved_rooms + nonroom_objs
+
+ scene_preprocessed = placement.camera.camera_selection_preprocessing(
+ terrain=None,
+ scene_objs=scene_objs
+ )
+
+ solved_floor_surface = butil.join_objects([
+ tagging.extract_tagged_faces(o, {t.Subpart.SupportSurface})
+ for o in solved_rooms
+ ])
+
+ placement.camera.configure_cameras(
+ camera_rigs,
+ scene_preprocessed=scene_preprocessed,
+ init_surfaces=solved_floor_surface
+ )
+
+ return scene_preprocessed
+
+ scene_preprocessed = p.run_stage('pose_cameras', pose_cameras, use_chance=False)
+
+ def animate_cameras():
+ cam_util.animate_cameras(camera_rigs, solved_bbox, scene_preprocessed, pois=[])
+ p.run_stage('animate_cameras', animate_cameras, use_chance=False, prereq='pose_cameras')
+
+ p.run_stage(
+ 'populate_intermediate_pholders',
+ populate.populate_state_placeholders,
+ solver.state,
+ filter=t.Semantics.AssetPlaceholderForChildren,
+ final=False,
+ use_chance=False
+ )
+
+ def solve_medium():
+ n_steps = overrides['solve_steps_medium']
+ for i, vars in enumerate(greedy.iterate_assignments(stages['on_wall'], state, all_vars, limits)):
+ solver.solve_objects(consgraph, stages['on_wall'], vars, n_steps, desc=f"on_wall_{i}")
+ for i, vars in enumerate(greedy.iterate_assignments(stages['on_ceiling'], state, all_vars, limits)):
+ solver.solve_objects(consgraph, stages['on_ceiling'], vars, n_steps, desc=f"on_ceiling_{i}")
+ for i, vars in enumerate(greedy.iterate_assignments(stages['side_obj'], state, all_vars, limits)):
+ solver.solve_objects(consgraph, stages['side_obj'], vars, n_steps, desc=f"side_obj_{i}")
+ return solver.state
+ state = p.run_stage('solve_medium', solve_medium, use_chance=False, default=state)
+
+ def solve_small():
+ n_steps = overrides['solve_steps_small']
+ for i, vars in enumerate(greedy.iterate_assignments(stages['obj_ontop_obj'], state, all_vars, limits)):
+ solver.solve_objects(consgraph, stages['obj_ontop_obj'], vars, n_steps, desc=f"obj_ontop_obj_{i}")
+ for i, vars in enumerate(greedy.iterate_assignments(stages['obj_on_support'], state, all_vars, limits)):
+ solver.solve_objects(consgraph, stages['obj_on_support'], vars, n_steps, desc=f"obj_on_support_{i}")
+ #for i, vars in enumerate(greedy.iterate_assignments(stages['tertiary'], state, all_vars, limits)):
+ # solver.solve_objects(consgraph, stages['tertiary'], vars, n_steps, desc=f"tertiary_{i}")
+ return solver.state
+ state = p.run_stage('solve_small', solve_small, use_chance=False, default=state)
+
+ p.run_stage('populate_assets', populate.populate_state_placeholders, state, use_chance=False)
+
+ door_filter = r.Domain({t.Semantics.Door}, [(cl.AnyRelation(), stages['rooms'])])
+ window_filter = r.Domain({t.Semantics.Window}, [(cl.AnyRelation(), stages['rooms'])])
+ p.run_stage('room_doors', lambda: room_dec.populate_doors(solver.get_bpy_objects(door_filter)), use_chance=False)
+ p.run_stage('room_windows', lambda: room_dec.populate_windows(solver.get_bpy_objects(window_filter)), use_chance=False)
+
+ room_meshes = solver.get_bpy_objects(r.Domain({t.Semantics.Room}))
+ p.run_stage('room_stairs', lambda: room_dec.room_stairs(state, room_meshes), use_chance=False)
+ p.run_stage('skirting_floor', lambda: make_skirting_board(room_meshes, t.Subpart.SupportSurface))
+ p.run_stage('skirting_ceiling', lambda: make_skirting_board(room_meshes, t.Subpart.Ceiling))
+
+ rooms_meshed = butil.get_collection('placeholders:room_meshes')
+ rooms_split = room_dec.split_rooms(list(rooms_meshed.objects))
+
+ p.run_stage('room_walls', room_dec.room_walls, rooms_split['wall'].objects, use_chance=False)
+ p.run_stage('room_pillars', room_dec.room_pillars, state, rooms_split['wall'].objects, use_chance=False)
+ p.run_stage('room_floors', room_dec.room_floors, rooms_split['floor'].objects, use_chance=False)
+ p.run_stage('room_ceilings', room_dec.room_ceilings, rooms_split['ceiling'].objects, use_chance=False)
+
+ #state.print()
+ state.to_json(output_folder / 'solve_state.json')
+
+ cam = cam_util.get_camera(0, 0)
+
+ def turn_off_lights():
+ for o in bpy.data.objects:
+ if o.type == 'LIGHT' and not o.data.cycles.is_portal:
+ print(f'Deleting {o.name}')
+ butil.delete(o)
+ p.run_stage('lights_off', turn_off_lights)
+
+ def invisible_room_ceilings():
+ rooms_split['exterior'].hide_viewport = True
+ rooms_split['exterior'].hide_render = True
+ invisible_to_camera.apply(list(rooms_split['ceiling'].objects))
+ invisible_to_camera.apply([o for o in bpy.data.objects if 'CeilingLight' in o.name])
+ p.run_stage('invisible_room_ceilings', invisible_room_ceilings, use_chance=False)
+
+ p.run_stage(
+ 'overhead_cam',
+ place_cam_overhead,
+ cam=camera_rigs[0],
+ bbox=solved_bbox,
+ use_chance=False
+ )
+
+ p.run_stage(
+ 'hide_other_rooms',
+ hide_other_rooms,
+ state,
+ rooms_split,
+ keep_rooms=[r.name for r in solved_rooms],
+ use_chance=False
+ )
+
+ height = p.run_stage(
+ 'nature_backdrop',
+ create_outdoor_backdrop,
+ terrain,
+ house_bbox=house_bbox,
+ cam=cam,
+ p=p,
+ params=overrides,
+ use_chance=False,
+ prereq='terrain',
+ default=0,
+ )
+
+ if overrides.get('topview', False):
+ rooms_split['exterior'].hide_viewport = True
+ rooms_split['ceiling'].hide_viewport = True
+ rooms_split['exterior'].hide_render = True
+ rooms_split['ceiling'].hide_render = True
+ for group in ['wall', 'floor']:
+ for wall in rooms_split[group].objects:
+ for mat in wall.data.materials:
+ for n in mat.node_tree.nodes:
+ if n.type == 'BSDF_PRINCIPLED':
+ n.inputs['Alpha'].default_value = overrides.get('alpha_walls', 1.)
+ bbox = np.concatenate([read_co(r) + np.array(r.location)[np.newaxis, :] for r in rooms_meshed.objects])
+ camera = camera_rigs[0].children[0]
+ camera_rigs[0].location = 0, 0, 0
+ camera_rigs[0].rotation_euler = 0, 0, 0
+ bpy.contexScene.camera = camera
+ rot_x = deg2rad(overrides.get('topview_rot_x', 0))
+ rot_z = deg2rad(overrides.get('topview_rot_z', 0))
+ camera.rotation_euler = rot_x, 0, rot_z
+ mean = np.mean(bbox, 0)
+ for cam_dist in np.exp(np.linspace(1., 5., 500)):
+ camera.location = mean[0] + cam_dist * np.sin(rot_x) * np.sin(rot_z), mean[1] - cam_dist * np.sin(
+ rot_x) * np.cos(rot_z), mean[2] - WALL_HEIGHT / 2 + cam_dist * np.cos(rot_x)
+ bpy.context.view_layer.update()
+ inview = points_inview(bbox, camera)
+ if inview.all():
+ for area in bpy.contexScreen.areas:
+ if area.type == 'VIEW_3D':
+ area.spaces.active.region_3d.view_perspective = 'CAMERA'
+ break
+ break
+
+ return {
+ "height_offset": height,
+ "whole_bbox": house_bbox,
+ }
+
+
+
+def main(args):
+ scene_seed = init.apply_scene_seed(args.seed)
+ init.apply_gin_configs(
+ configs=args.configs,
+ overrides=args.overrides,
+ configs_folder='infinigen_examples/configs_indoor'
+ )
+ constants.initialize_constants()
+
+ execute_tasks.main(compose_scene_func=compose_indoors, input_folder=args.input_folder,
+ output_folder=args.output_folder, task=args.task, task_uniqname=args.task_uniqname,
+ scene_seed=scene_seed)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--output_folder', type=Path)
+ parser.add_argument('--input_folder', type=Path, default=None)
+ parser.add_argument('-s', '--seed', default=None, help="The seed used to generate the scene")
+ parser.add_argument('-t', '--task', nargs='+', default=['coarse'],
+ choices=['coarse', 'populate', 'fine_terrain', 'ground_truth', 'render', 'mesh_save', 'export'])
+ parser.add_argument('-g', '--configs', nargs='+', default=['base'],
+ help='Set of config files for gin (separated by spaces) '
+ 'e.g. --gin_config file1 file2 (exclude .gin from path)')
+ parser.add_argument('-p', '--overrides', nargs='+', default=[],
+ help='Parameter settings that override config defaults '
+ 'e.g. --gin_param module_1.a=2 module_2.b=3')
+ parser.add_argument('--task_uniqname', type=str, default=None)
+ parser.add_argument('-d', '--debug', type=str, nargs='*', default=None)
+
+ args = init.parse_args_blender(parser)
+ logging.getLogger("infinigen").setLevel(logging.INFO)
+ logging.getLogger("infinigen.core.nodes.node_wrangler").setLevel(logging.CRITICAL)
+
+ if args.debug is not None:
+ for name in logging.root.manager.loggerDict:
+ if not name.startswith('infinigen'):
+ continue
+ if len(args.debug) == 0 or any(name.endswith(x) for x in args.debug):
+ logging.getLogger(name).setLevel(logging.DEBUG)
+
+ main(args)
\ No newline at end of file
diff --git a/infinigen_examples/generate_material_balls.py b/infinigen_examples/generate_material_balls.py
new file mode 100644
index 000000000..6a6c4be8c
--- /dev/null
+++ b/infinigen_examples/generate_material_balls.py
@@ -0,0 +1,203 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this
+# source tree.
+
+# Authors:
+# - Lingjie Mei
+# - Alex Raistrick
+# - Karhan Kayan - add fire option
+
+import argparse
+import importlib
+import math
+import os
+import random
+import re
+import subprocess
+import traceback
+from itertools import product
+from pathlib import Path
+import logging
+
+from numpy.random import uniform
+from tqdm import tqdm
+
+from infinigen.assets.materials import metal_shader_list
+from infinigen.assets.materials.woods import tiled_wood
+from infinigen_examples.generate_individual_assets import adjust_cam_distance, make_args, parent, setup_camera
+from infinigen_examples.util.test_utils import load_txt_list
+
+logging.basicConfig(format='[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] | %(message)s',
+ datefmt='%H:%M:%S', level=logging.WARNING)
+
+import bpy
+import gin
+import numpy as np
+from PIL import Image
+
+from infinigen.assets.fluid.fluid import set_obj_on_fire
+from infinigen.core.tagging import tag_system
+from infinigen.assets.lighting import sky_lighting, hdri_lighting, three_point_lighting, holdout_lighting
+
+from infinigen.core import surface, init
+from infinigen.core.placement import density, factory
+from infinigen.core.util.camera import points_inview
+
+from infinigen.assets.utils.misc import assign_material, subclasses
+from infinigen.core.rendering.render import enable_gpu
+from infinigen.assets.utils.decorate import read_base_co, read_co
+
+from infinigen.core.util.math import FixedSeed
+# noinspection PyUnresolvedReferences
+from infinigen.core.util import blender as butil
+
+
+def build_scene_surface(factory_name, idx):
+ try:
+ with gin.unlock_config():
+ try:
+ template = importlib.import_module(f'infinigen.assets.materials.{factory_name}')
+ except:
+ for subdir in os.listdir('infinigen/assets/materials'):
+ with gin.unlock_config():
+ module = importlib.import_module(f'infinigen.assets.materials.{subdir.split(".")[0]}')
+ if hasattr(module, factory_name):
+ template = getattr(module, factory_name)
+ break
+ else:
+ raise Exception(f'{factory_name} not Found.')
+ if type(template) is type:
+ template = template(idx)
+ if hasattr(template, 'make_sphere'):
+ asset = template.make_sphere()
+ else:
+ bpy.ops.mesh.primitive_ico_sphere_add(radius=1, subdivisions=7)
+ asset = bpy.context.active_object
+
+ with FixedSeed(idx):
+ if 'metal' in factory_name or 'sofa_fabric' in factory_name:
+ template.apply(asset, scale=.1)
+ elif 'hardwood' in factory_name:
+ template.apply(asset, rotation=(np.pi / 2, 0, 0))
+ elif 'brick' in factory_name:
+ template.apply(asset, height=uniform(.25, .3))
+ else:
+ template.apply(asset)
+ except ModuleNotFoundError:
+ raise Exception(f'{factory_name} not Found.')
+ return asset
+
+
+def build_scene(path, factory_names, args):
+ scene = bpy.context.scene
+ scene.render.engine = 'CYCLES'
+ scene.render.resolution_x, scene.render.resolution_y = map(int, args.resolution.split('x'))
+ scene.cycles.samples = args.samples
+ butil.clear_scene()
+
+ if not args.fire:
+ bpy.context.scene.render.film_transparent = args.film_transparent
+ bpy.context.scene.world.node_tree.nodes['Background'].inputs[0].default_value[-1] = 0
+ camera, center = setup_camera(args)
+
+ scale = .3
+ assets = []
+ with tqdm(total=len(factory_names)) as pbar:
+ for idx, factory_name in enumerate(factory_names):
+ asset = build_scene_surface(factory_name, idx)
+ assets.append(asset)
+ asset.name = factory_name
+ pbar.update(1)
+ margin = scale * 2.2
+ size = 5
+ for i in range(len(assets)):
+ assets[i].scale = [scale] * 3
+ butil.apply_transform(assets[i])
+ assets[i].location = (i // size) * margin, (i % size) * margin, scale
+
+ bpy.ops.mesh.primitive_grid_add(size=1, x_subdivisions=400, y_subdivisions=400)
+ asset = bpy.context.active_object
+ asset.scale = [scale * len(assets) / size * 4] * 3
+ asset.location = (len(assets) // size - 1) * margin / 2, size // 2 * margin * .8, 0
+ tiled_wood.apply(asset, hscale=10, vscale=3)
+
+ with FixedSeed(args.lighting):
+ if args.hdri:
+ hdri_lighting.add_lighting()
+ elif args.three_point:
+ holdout_lighting.add_lighting()
+ three_point_lighting.add_lighting(asset)
+ else:
+ sky_lighting.add_lighting(camera)
+ nodes = bpy.data.worlds['World'].node_tree.nodes
+ sky_texture = [n for n in nodes if n.name.startswith('Sky Texture')][-1]
+ sky_texture.sun_elevation = np.deg2rad(args.elevation)
+ sky_texture.sun_rotation = np.pi * .75
+
+ if args.scale_reference:
+ bpy.ops.mesh.primitive_cylinder_add(radius=0.3, depth=1.8, location=(4.9, 4.9, 1.8 / 2))
+
+ if args.cam_center > 0 and asset:
+ co = read_base_co(asset) + asset.location
+ center.location = (np.amin(co, 0) + np.amax(co, 0)) / 2
+ center.location[-1] += args.cam_zoff
+
+ if args.cam_dist <= 0 and asset:
+ adjust_cam_distance(asset, camera, args.margin, .6)
+
+ cam_info_ng = bpy.data.node_groups.get('nodegroup_active_cam_info')
+ if cam_info_ng is not None:
+ cam_info_ng.nodes['Object Info'].inputs['Object'].default_value = camera
+
+ if args.save_blend:
+ (path / 'scenes').mkdir(exist_ok=True)
+ butil.save_blend(f"{path}/scenes/scene_{idx:03d}.blend", autopack=True)
+
+
+def main(args):
+ bpy.context.window.workspace = bpy.data.workspaces['Geometry Nodes']
+
+ init.apply_gin_configs('infinigen_examples/configs_indoor', skip_unknown=True)
+ surface.registry.initialize_from_gin()
+
+ extras = '[%(filename)s:%(lineno)d] ' if args.loglevel == logging.DEBUG else ''
+ logging.basicConfig(format=f'[%(asctime)s.%(msecs)03d] [%(name)s] [%(levelname)s] {extras}| %(message)s',
+ level=args.loglevel, datefmt='%H:%M:%S')
+ logging.getLogger("infinigen").setLevel(args.loglevel)
+
+ if '.txt' in args.factories[0]:
+ name = args.factories[0].split('.')[-2].split('/')[-1]
+ else:
+ name = '_'.join(args.factories)
+ path = Path(os.getcwd()) / 'outputs' / name
+ path.mkdir(exist_ok=True)
+
+ if args.gpu:
+ enable_gpu()
+
+ factories = list(args.factories)
+ if 'ALL_ASSETS' in factories:
+ factories += [f.__name__ for f in subclasses(factory.AssetFactory)]
+ factories.remove('ALL_ASSETS')
+ if 'ALL_SCATTERS' in factories:
+ factories += [f.stem for f in Path('surfaces/scatters').iterdir()]
+ factories.remove('ALL_SCATTERS')
+ if 'ALL_MATERIALS' in factories:
+ factories += [f.stem for f in Path('infinigen/assets/materials').iterdir()]
+ factories.remove('ALL_MATERIALS')
+ if '.txt' in factories[0]:
+ factories = [f.split('.')[-1] for f in load_txt_list(factories[0], skip_sharp=False)]
+
+ try:
+ build_scene(path, factories, args)
+ except Exception as e:
+ print(e)
+
+
+if __name__ == '__main__':
+ args = make_args()
+ args.no_mod = args.no_mod or args.fire
+ args.film_transparent = args.film_transparent and not args.hdri
+ with FixedSeed(1):
+ main(args)
diff --git a/infinigen_examples/generate_nature.py b/infinigen_examples/generate_nature.py
index 6a78ec178..0a4d7cfee 100644
--- a/infinigen_examples/generate_nature.py
+++ b/infinigen_examples/generate_nature.py
@@ -67,8 +67,8 @@
from infinigen.core import execute_tasks, surface, init
@gin.configurable
-def compose_scene(output_folder, scene_seed, **params):
-
+def compose_nature(output_folder, scene_seed, **params):
+
p = pipeline.RandomStageExecutor(scene_seed, output_folder, params)
def add_coarse_terrain():
@@ -82,7 +82,7 @@ def add_coarse_terrain():
terrain_mesh = butil.create_noise_plane()
density.set_tag_dict({})
- terrain_bvh = mathutils.bvhtree.BVHTree.FromObject(terrain_mesh, bpy.context.evaluated_depsgraph_get())
+ scene_bvh = mathutils.bvhtree.BVHTree.FromObject(terrain_mesh, bpy.context.evaluated_depsgraph_get())
land_domain = params.get('land_domain_tags')
underwater_domain = params.get('underwater_domain_tags')
@@ -168,14 +168,20 @@ def add_cactus(terrain_mesh):
def camera_preprocess():
camera_rigs = cam_util.spawn_camera_rigs()
- scene_preprocessed = cam_util.camera_selection_preprocessing(terrain, terrain_mesh)
+ scene_preprocessed = cam_util.camera_selection_preprocessing(
+ terrain,
+ terrain_mesh,
+ tags_ratio=params.get('camera_selection_tags_ratio'),
+ ranges_ratio=params.get('camera_selection_ranges_ratio'),
+ anim_criterion_keys=params.get('camera_selection_anim_criterion_keys', False),
+ )
return camera_rigs, scene_preprocessed
camera_rigs, scene_preprocessed = p.run_stage('camera_preprocess', camera_preprocess, use_chance=False)
bbox = terrain.get_bounding_box() if terrain is not None else butil.bounds(terrain_mesh)
p.run_stage(
'pose_cameras',
- lambda: cam_util.configure_cameras(camera_rigs, bbox, scene_preprocessed),
+ lambda: cam_util.configure_cameras(camera_rigs, scene_preprocessed, init_bounding_box=bbox),
use_chance=False
)
cam = cam_util.get_camera(0, 0)
@@ -194,7 +200,7 @@ def camera_preprocess():
def add_ground_creatures(target):
fac_class = sample_registry(params['ground_creature_registry'])
- fac = fac_class(int_hash((scene_seed, 0)), bvh=terrain_bvh, animation_mode='idle')
+ fac = fac_class(int_hash((scene_seed, 0)), bvh=scene_bvh, animation_mode='idle')
n = params.get('max_ground_creatures', randint(1, 4))
selection = density.placement_mask(select_thresh=0, tag='beach', altitude_range=(-0.5, 0.5)) \
if fac_class is creatures.CrabFactory else 1
@@ -204,14 +210,14 @@ def add_ground_creatures(target):
def flying_creatures():
fac_class = sample_registry(params['flying_creature_registry'])
- fac = fac_class(randint(1e7), bvh=terrain_bvh, animation_mode='idle')
+ fac = fac_class(randint(1e7), bvh=scene_bvh, animation_mode='idle')
n = params.get('max_flying_creatures', randint(2, 7))
col = placement.scatter_placeholders_mesh(terrain_center, fac, num_placeholders=n, overall_density=1, altitude=0.2)
return list(col.objects)
pois += p.run_stage('flying_creatures', flying_creatures, default=[])
p.run_stage('animate_cameras', lambda: cam_util.animate_cameras(
- camera_rigs, scene_preprocessed, pois=pois), use_chance=False)
+ camera_rigs, bbox, scene_preprocessed, pois=pois), use_chance=False)
with logging_util.Timer('Compute coarse terrain frustrums'):
terrain_inview, *_ = split_in_view.split_inview(
@@ -394,22 +400,25 @@ def add_snow_particles():
p.run_stage('tilted_river', add_tilted_river, use_chance=False)
p.save_results(output_folder/'pipeline_coarse.csv')
- return terrain, terrain_mesh
+ return {
+ "height_offset": 0,
+ "whole_bbox": None,
+ }
def main(args):
scene_seed = init.apply_scene_seed(args.seed)
- mandatory_exclusive = [Path('infinigen_examples/configs/scene_types')]
+ mandatory_exclusive = [Path('infinigen_examples/configs_nature/scene_types')]
init.apply_gin_configs(
configs=args.configs,
overrides=args.overrides,
- configs_folder='infinigen_examples/configs',
+ configs_folder='infinigen_examples/configs_nature',
mandatory_folders=mandatory_exclusive,
mutually_exclusive_folders=mandatory_exclusive,
)
execute_tasks.main(
- compose_scene_func=compose_scene,
+ compose_scene_func=compose_nature,
input_folder=args.input_folder,
output_folder=args.output_folder,
task=args.task,
@@ -424,7 +433,7 @@ def main(args):
parser.add_argument('--input_folder', type=Path, default=None)
parser.add_argument('-s', '--seed', default=None, help="The seed used to generate the scene")
parser.add_argument('-t', '--task', nargs='+', default=['coarse'],
- choices=['coarse', 'populate', 'fine_terrain', 'ground_truth', 'render', 'mesh_save'])
+ choices=['coarse', 'populate', 'fine_terrain', 'ground_truth', 'render', 'mesh_save', 'export'])
parser.add_argument('-g', '--configs', nargs='+', default=['base'],
help='Set of config files for gin (separated by spaces) '
'e.g. --gin_config file1 file2 (exclude .gin from path)')
diff --git a/infinigen_examples/indoor_asset_semantics.py b/infinigen_examples/indoor_asset_semantics.py
new file mode 100644
index 000000000..9ae3196c2
--- /dev/null
+++ b/infinigen_examples/indoor_asset_semantics.py
@@ -0,0 +1,345 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree.
+
+# Authors: Alexander Raistrick
+
+from infinigen.assets import (
+ appliances,
+ bathroom,
+ decor,
+ elements,
+ lighting,
+ seating,
+ shelves,
+ table_decorations,
+ tables,
+ tableware,
+ wall_decorations,
+ windows,
+ clothes
+)
+
+from infinigen.core.tags import Semantics, Subpart, FromGenerator
+
+def home_asset_usage():
+
+ """ Defines what generators are consider to fulfill what roles in a home setting.
+
+ The primary effect of this to determine what types of objects are returned by the square brackets [ ] operator in home_constraints
+
+ You can define these however you like - use
+
+ See the `Semantics` class in `infinigen.core.tags` for a list of possible semantics, or add your own.
+
+ """
+
+ # TODO: this whole used_as will be integrated into the constraint language. Currently there are two paralell semantics trees, one to define the tags and one to use them.
+
+ used_as = {}
+
+ # region small objects
+
+ used_as[Semantics.Dishware] = {
+ tableware.PlateFactory,
+ tableware.BowlFactory,
+ tableware.WineglassFactory,
+ tableware.PanFactory,
+ tableware.PotFactory,
+ tableware.CupFactory,
+ }
+ used_as[Semantics.Cookware] = {
+ tableware.PotFactory,
+ tableware.PanFactory
+ }
+ used_as[Semantics.Utensils] = {
+ tableware.SpoonFactory,
+ tableware.KnifeFactory,
+ tableware.ChopsticksFactory,
+ tableware.ForkFactory,
+ }
+
+ used_as[Semantics.FoodPantryItem] = {
+ tableware.CanFactory,
+ tableware.FoodBagFactory,
+ tableware.FoodBoxFactory,
+ tableware.JarFactory,
+ tableware.BottleFactory
+ }
+
+ used_as[Semantics.TableDisplayItem] = {
+ tableware.FruitContainerFactory,
+ table_decorations.VaseFactory,
+ tableware.BowlFactory,
+ tableware.PotFactory
+ }
+
+ used_as[Semantics.OfficeShelfItem] = {
+ table_decorations.BookStackFactory,
+ table_decorations.BookColumnFactory,
+ elements.NatureShelfTrinketsFactory,
+ }
+
+ used_as[Semantics.KitchenCounterItem] = set().union(
+ used_as[Semantics.Dishware],
+ used_as[Semantics.Cookware],
+ {
+ table_decorations.BookColumnFactory,
+ tableware.JarFactory,
+ }
+ )
+
+ used_as[Semantics.BathroomItem] = {
+ tableware.BottleFactory,
+ tableware.BowlFactory,
+ clothes.TowelFactory,
+ }
+
+ used_as[Semantics.ClothDrapeItem] = {
+ # objects that can be strewn about / draped over furniture
+ #clothes.BlanketFactory,
+ clothes.PantsFactory,
+ clothes.ShirtFactory,
+ }
+
+ used_as[Semantics.HandheldItem] = set.union(
+ used_as[Semantics.Utensils],
+ used_as[Semantics.FoodPantryItem],
+ used_as[Semantics.TableDisplayItem],
+ used_as[Semantics.OfficeShelfItem],
+ used_as[Semantics.ClothDrapeItem],
+ used_as[Semantics.Dishware],
+ )
+
+ # endregion
+
+ # region furniture
+
+ used_as[Semantics.Sink] = {
+ table_decorations.SinkFactory,
+ bathroom.BathroomSinkFactory,
+ bathroom.StandingSinkFactory
+ }
+
+ used_as[Semantics.Storage] = {
+ shelves.SimpleBookcaseFactory,
+ shelves.CellShelfFactory,
+ shelves.LargeShelfFactory,
+ shelves.KitchenCabinetFactory,
+ shelves.SingleCabinetFactory
+ }
+
+ used_as[Semantics.SideTable] = {
+ shelves.SidetableDeskFactory,
+ tables.SideTableFactory
+ }
+
+ used_as[Semantics.Table] = set.union(
+ used_as[Semantics.SideTable],
+ {
+ tables.TableDiningFactory,
+ tables.TableCocktailFactory,
+ shelves.SimpleDeskFactory,
+ tables.CoffeeTableFactory,
+ }
+ )
+
+ used_as[Semantics.Chair] = {
+ seating.BarChairFactory,
+ seating.ChairFactory,
+ seating.OfficeChairFactory
+ }
+
+ used_as[Semantics.LoungeSeating] = {
+ seating.SofaFactory,
+ seating.ArmChairFactory,
+ }
+
+ used_as[Semantics.Seating] = set.union(
+ used_as[Semantics.Chair],
+ used_as[Semantics.LoungeSeating],
+ )
+
+ used_as[Semantics.KitchenAppliance] = {
+ appliances.DishwasherFactory,
+ appliances.OvenFactory,
+ appliances.BeverageFridgeFactory,
+ appliances.MicrowaveFactory
+ }
+
+ used_as[Semantics.KitchenCounter] = {
+ shelves.KitchenSpaceFactory,
+ shelves.KitchenIslandFactory,
+ }
+
+ used_as[Semantics.Bed] = {
+ seating.BedFactory,
+ }
+
+ used_as[Semantics.Furniture] = set().union(
+ used_as[Semantics.Storage],
+ used_as[Semantics.Table],
+ used_as[Semantics.Seating],
+ used_as[Semantics.KitchenCounter],
+ used_as[Semantics.KitchenAppliance],
+ used_as[Semantics.Bed],
+ {
+ bathroom.StandingSinkFactory,
+ bathroom.ToiletFactory,
+ bathroom.BathtubFactory,
+
+ seating.SofaFactory,
+ shelves.TVStandFactory,
+ }
+ )
+
+ # endregion furniture
+
+ used_as[Semantics.WallDecoration] = {
+ wall_decorations.WallArtFactory,
+ wall_decorations.MirrorFactory,
+ wall_decorations.BalloonFactory
+ }
+
+ used_as[Semantics.Door] = {
+ elements.doors.GlassPanelDoorFactory,
+ elements.doors.LiteDoorFactory,
+ elements.doors.LouverDoorFactory,
+ elements.doors.PanelDoorFactory,
+ }
+
+ used_as[Semantics.Window] = {
+ windows.WindowFactory
+ }
+
+ used_as[Semantics.CeilingLight] = {
+ lighting.CeilingLightFactory,
+ }
+
+ used_as[Semantics.Lighting] = set().union(
+ used_as[Semantics.CeilingLight],
+ {
+ lighting.LampFactory,
+ lighting.FloorLampFactory,
+ lighting.DeskLampFactory,
+ }
+ )
+
+ used_as[Semantics.Object] = set().union(
+
+ used_as[Semantics.Furniture],
+ used_as[Semantics.Sink],
+ used_as[Semantics.Door],
+ used_as[Semantics.Window],
+ used_as[Semantics.WallDecoration],
+ used_as[Semantics.HandheldItem],
+ used_as[Semantics.Lighting],
+ {
+ tableware.PlantContainerFactory,
+ tableware.LargePlantContainerFactory,
+ decor.AquariumTankFactory,
+
+ appliances.TVFactory,
+ appliances.MonitorFactory,
+
+ elements.RugFactory,
+ bathroom.HardwareFactory,
+ }
+ )
+
+ # region Extra metadata about assets
+ # TODO be move outside of the semantics heirarchy and into separate AssetFactory.metadata classvar
+
+ used_as[Semantics.RealPlaceholder] = {
+ appliances.MonitorFactory,
+ appliances.TVFactory,
+
+ bathroom.BathroomSinkFactory,
+ bathroom.StandingSinkFactory,
+ bathroom.ToiletFactory,
+
+ decor.AquariumTankFactory,
+ elements.RackFactory,
+ elements.RugFactory,
+
+ seating.BedFrameFactory,
+ seating.BedFactory,
+ seating.ChairFactory,
+
+ shelves.KitchenSpaceFactory,
+
+ tables.TableCocktailFactory,
+
+ table_decorations.BookColumnFactory,
+ table_decorations.BookFactory,
+ table_decorations.BookStackFactory,
+ table_decorations.SinkFactory,
+
+ tableware.BowlFactory,
+ tableware.FoodBoxFactory,
+ tableware.FruitContainerFactory,
+ tableware.LargePlantContainerFactory,
+ tableware.PlantContainerFactory,
+ tableware.PotFactory,
+
+ wall_decorations.BalloonFactory,
+ wall_decorations.MirrorFactory,
+ wall_decorations.WallArtFactory,
+ shelves.SingleCabinetFactory,
+ shelves.KitchenCabinetFactory,
+ shelves.CellShelfFactory,
+ elements.NatureShelfTrinketsFactory
+ }
+
+ used_as[Semantics.AssetAsPlaceholder] = set()
+
+ used_as[Semantics.AssetPlaceholderForChildren] = {
+ shelves.SimpleBookcaseFactory,
+ shelves.CellShelfFactory,
+ shelves.SingleCabinetFactory,
+ shelves.KitchenCabinetFactory,
+ shelves.LargeShelfFactory,
+ table_decorations.SinkFactory,
+ tables.TableCocktailFactory
+ }
+
+ used_as[Semantics.PlaceholderBBox] = {
+ seating.SofaFactory,
+ appliances.OvenFactory,
+ }
+
+ used_as[Semantics.SingleGenerator] = set().union(
+ used_as[Semantics.Dishware],
+ used_as[Semantics.Utensils],
+ {
+ lighting.CeilingLightFactory,
+ lighting.CeilingClassicLampFactory,
+ seating.ChairFactory,
+ seating.BarChairFactory,
+ seating.OfficeChairFactory
+ }
+ ).difference({
+ tableware.CupFactory
+ })
+
+ used_as[Semantics.NoRotation] = set().union(
+ used_as[Semantics.WallDecoration],
+ {
+ bathroom.HardwareFactory,
+ lighting.CeilingLightFactory, # rotationally symetric
+ }
+ )
+
+ used_as[Semantics.NoCollision] = {
+ elements.RugFactory,
+ }
+
+ used_as[Semantics.NoChildren] = {
+ elements.RugFactory,
+ wall_decorations.MirrorFactory,
+ wall_decorations.WallArtFactory,
+ lighting.CeilingLightFactory,
+ }
+
+ # endregion
+
+ return used_as
\ No newline at end of file
diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py
new file mode 100644
index 000000000..cda7d78b3
--- /dev/null
+++ b/infinigen_examples/indoor_constraint_examples.py
@@ -0,0 +1,807 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from collections import OrderedDict
+
+import numpy as np
+import random
+from numpy.random import uniform, normal, randint
+
+import infinigen
+import gin
+
+from infinigen.assets import (
+ appliances,
+ bathroom,
+ decor,
+ elements,
+ lighting,
+ seating,
+ shelves,
+ table_decorations,
+ tables,
+ tableware,
+ wall_decorations,
+ windows,
+ clothes
+)
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ example_solver,
+ usage_lookup
+)
+
+from infinigen import assets
+
+from infinigen.core.util.math import clip_gaussian
+from infinigen.core.tags import Semantics, Subpart, FromGenerator
+
+from .indoor_asset_semantics import home_asset_usage
+from .util import constraint_util as cu
+
+def sample_home_constraint_params():
+ return dict(
+ has_tv = uniform() < 0.5,
+ has_aquarium_tank = uniform() < 0.15,
+ has_birthday_balloons = uniform() < 0.15,
+ has_cocktail_tables = uniform() < 0.15,
+ has_kitchen_barstools = uniform() < 0.15,
+ )
+
+def home_constraints():
+
+ """Construct a constraint graph which incentivizes realistic home layouts.
+
+ Result will contain both hard constraints (`constraints`) and soft constraints (`score_terms`).
+
+ Notes for developers:
+ - This function is typically evaluated ONCE. It is not called repeatedly during the optimization process.
+ - To debug values you will need to inject print statements into impl_bindings.py or evaluate.py. Better debugging tools will come soon.
+ - Similarly, most `lambda:` statements below will only be evaluated once to construct the graph - do not assume they will be re-evaluated during optimization.
+ - Available constraint options are in `infinigen/core/constraints/constraint_language/__init__.py`.
+ - You can easily add new constraint functions by adding them here, and defining evaluator functions for them in `impl_bindings.py`
+ - Using newly added constraint types as hard constraints may be rejected by our hard constraint solver
+ - It is quite easy to specify an impossible constraint program, or one that our solver cannot solve:
+ - By default, failing to solve the program correctly is just printed as a warning, and we still return the scene.
+ - You can cause failed optimization results to crash instead using `-p solve_objects.abort_unsatisfied=True` in the command line.
+ - More documentation coming soon, and feel free to ask questions on Github Issues!
+
+ """
+
+ used_as = home_asset_usage()
+ usage_lookup.initialize_from_dict(used_as)
+
+ rooms = cl.scene()[{Semantics.Room, -Semantics.Object}]
+ obj = cl.scene()[{Semantics.Object, -Semantics.Room}]
+
+ cutters = cl.scene()[Semantics.Cutter]
+ window = cutters[Semantics.Window]
+ doors = cutters[Semantics.Door]
+
+ constraints = OrderedDict()
+ score_terms = OrderedDict()
+
+ #region overall fullness
+
+ furniture = obj[Semantics.Furniture].related_to(rooms, cu.on_floor)
+ wallfurn = furniture.related_to(rooms, cu.against_wall)
+ storage = wallfurn[Semantics.Storage]
+
+ params = sample_home_constraint_params()
+
+ for k, v in params.items():
+ print(f"{home_constraints.__name__} params - {k}: {v}")
+
+ score_terms['fullness'] = rooms.sum(lambda r: (
+ obj.count().maximize(weight=4) # TODO re-incorporate more precise fullness scores above
+ + obj.volume().maximize(weight=1)
+ ))
+
+ #endregion
+
+ #region furniture
+
+ score_terms['furniture_aesthetics'] = wallfurn.sum(lambda t: (
+ t.distance(wallfurn).hinge(0.2, 0.6).maximize(weight=0.6) +
+ cl.accessibility_cost(t, furniture).minimize(weight=5) +
+ cl.accessibility_cost(t, rooms).minimize(weight=10)
+ ))
+
+
+ constraints['storage'] = rooms.all(lambda r: (
+ storage.related_to(r).count().in_range(1, 7)
+ ))
+ score_terms['storage'] = rooms.sum(lambda r: (
+ cl.accessibility_cost(storage.related_to(r), furniture.related_to(r), dist=0.5).minimize(weight=5)
+ + cl.accessibility_cost(storage.related_to(r), r, dist=0.5).minimize(weight=5)
+ ))
+
+ #endregion furntiure
+
+ score_terms['portal_accessibility'] = (
+ # make sure the fronts of objects are accessible where applicable
+
+ #### disabled since its generally fine to block floor-to-ceiling windows a little
+ #window.sum(lambda t: (
+ # cl.accessibility_cost(t, furniture, np.array([0, -1, 0]))
+ #)).minimize(weight=2) +
+
+ doors.sum(lambda t: (
+ cl.accessibility_cost(t, furniture, cu.front_dir, dist=4) +
+ cl.accessibility_cost(t, furniture, cu.back_dir, dist=4)
+ )).minimize(weight=5)
+ )
+
+ #region WALL/FLOOR COVERINGS
+ walldec = obj[Semantics.WallDecoration].related_to(rooms, cu.flush_wall)
+ wall_art = walldec[wall_decorations.WallArtFactory]
+ mirror = walldec[wall_decorations.MirrorFactory]
+ rugs = obj[elements.RugFactory].related_to(rooms, cu.on_floor)
+
+ constraints['rugs'] = rooms.all(lambda r: (
+ rugs.related_to(r).distance(rugs) >= 1
+ ))
+
+ score_terms['rugs'] = rooms.all(lambda r: (
+ cl.center_stable_surface_dist(rugs.related_to(r)).minimize(weight=1)
+ ))
+
+ vertical_diff = lambda o, r: (o.distance(r, cu.floortags) - o.distance(r, cu.ceilingtags)).abs()
+
+ constraints['wall_decorations'] = rooms.all(lambda r: (
+ wall_art.related_to(r).count().in_range(0, 6)
+ * mirror.related_to(r).count().in_range(0, 1)
+ * walldec.related_to(r).all(lambda t: t.distance(r, cu.floortags) > 0.6)
+ #walldec.all(lambda t: (
+ # (vertical_diff(t, r).abs() < 1.5) *
+ # (t.distance(cutters) > 0.1)
+ #))
+ ))
+ score_terms['wall_decorations'] = rooms.sum(lambda r: (
+
+ walldec.related_to(r).sum(lambda w: (
+
+ vertical_diff(w, r).abs().minimize(weight=1)
+ + w.distance(walldec).maximize(weight=1)
+ + w.distance(window).hinge(0.25, 10).maximize(weight=1)
+
+ + cl.angle_alignment_cost(w, r, cu.floortags).minimize(weight=5)
+ + cl.accessibility_cost(w, furniture, dist=1).minimize(weight=5)
+ + cl.center_stable_surface_dist(w).minimize(weight=1)
+ ))
+ ))
+
+ score_terms['floor_covering'] = (
+ rugs.sum(lambda rug: (
+ rug.distance(rooms, cu.walltags).maximize(weight=3) +
+ cl.angle_alignment_cost(rug, rooms, cu.walltags).minimize(weight=3)
+ ))
+ )
+ #endregion
+
+ #region PLANTS
+ small_plants = obj[tableware.PlantContainerFactory].related_to(storage, cu.ontop)
+ big_plants = (
+ obj[tableware.LargePlantContainerFactory]
+ .related_to(rooms, cu.on_floor)
+ .related_to(rooms, cu.against_wall)
+ )
+ constraints['plants'] = rooms.all(lambda r: (
+ big_plants.related_to(r).count().in_range(0, 1) *
+ small_plants.related_to(storage.related_to(r)).count().in_range(0, 5)
+ ))
+ score_terms['plants'] = rooms.sum(lambda r: (
+
+ big_plants.related_to(r).sum(lambda p: p.distance(doors)).maximize(weight=5)
+
+ + ( # small plants should be near window for sunlight
+ small_plants
+ .related_to(storage.related_to(r))
+ .sum(lambda p: p.distance(window.related_to(r)))
+ ).minimize(weight=1)
+ ))
+ #endregion
+
+ #region SIDETABLE
+ sidetable = furniture[Semantics.SideTable].related_to(furniture, cu.side_by_side)
+
+ score_terms['sidetable'] = rooms.sum(lambda r: (
+ sidetable.related_to(r).sum(lambda t: (
+ t.distance(r, cu.walltags).minimize(weight=1)
+ ))
+ ))
+ #endregion
+
+ #region DESKS
+ desks = wallfurn[shelves.SimpleDeskFactory]
+ deskchair = furniture[seating.OfficeChairFactory].related_to(desks, cu.front_against)
+ monitors = obj[appliances.MonitorFactory]
+ constraints['desk'] = rooms.all(lambda r: (
+ desks.related_to(r).all(lambda t: (
+ deskchair.related_to(r).related_to(t).count().in_range(0, 1) *
+ monitors.related_to(t, cu.ontop).count().equals(1) *
+ (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count() >= 0) *
+ (deskchair.related_to(r).related_to(t).count() == 1)
+ ))
+ ))
+
+ score_terms['desk'] = rooms.sum(lambda r: desks.sum(lambda d: (
+
+ obj.related_to(d).count().maximize(weight=3)
+
+ + d.distance(doors.related_to(r)).maximize(weight=0.1)
+
+ + cl.accessibility_cost(d, furniture.related_to(r)).minimize(weight=3)
+ + cl.accessibility_cost(d, r).minimize(weight=3)
+
+ + monitors.related_to(d).sum(lambda m: (
+ cl.accessibility_cost(m, r, dist=2).minimize(weight=3) +
+ cl.accessibility_cost(m, obj.related_to(r), dist=0.5).minimize(weight=3) +
+ m.distance(r, cu.walltags).hinge(0.1, 1e7).minimize(weight=1)
+ ))
+
+ + deskchair.distance(rooms, cu.walltags).maximize(weight=1)
+ )))
+
+ #endregion
+
+ #region ALL LIGHTING RULES
+
+ lights = obj[Semantics.Lighting]
+ floor_lamps = lights[lighting.FloorLampFactory].related_to(rooms, cu.on_floor).related_to(rooms, cu.against_wall)
+ #constraints['lighting'] = rooms.all(lambda r: (
+ # # dont put redundant lights close to eachother (including lamps, ceiling lights, etc)
+ # lights.related_to(r).all(lambda l: l.distance(lights.related_to(r)) >= 2)
+ #))
+
+ #endregion
+
+ #region CEILING LIGHTS
+ ceillights = lights[lighting.CeilingLightFactory]
+
+ constraints['ceiling_lights'] = rooms.all(lambda r: (
+ ceillights.related_to(r, cu.hanging).count().in_range(1, 4)
+ ))
+ score_terms['ceiling_lights'] = rooms.sum(lambda r: (
+ (ceillights.count() / r.volume(dims=2)).hinge(0.08, 0.15).minimize(weight=5) +
+ ceillights.mean(lambda t: (
+ t.distance(r, cu.walltags).pow(0.5) * 1.5 +
+ t.distance(ceillights).pow(0.2) * 2
+ )).maximize(weight=1)
+ ))
+ #endregion
+
+ #region LAMPS
+ lamps = lights[lighting.DeskLampFactory].related_to(furniture, cu.ontop)
+ constraints['lamps'] = rooms.all(lambda r: (
+
+ # allow 0-2 lamps per room, placed on any sensible object
+ lamps.related_to(storage.related_to(r)).count().in_range(0, 2)
+ #* lamps.related_to(sidetable.related_to(r)).count().in_range(0, 2)
+ #* lamps.related_to(desks.related_to(r, cu.on), cu.ontop).count().in_range(0, 1)
+
+ * ( # pull-string lamps look extremely unnatural when too far off the ground
+ lamps.related_to(storage.related_to(r))
+ .all(lambda l:
+ l.distance(r, cu.floortags).in_range(0.5, 1.5)
+ )
+ )
+
+ ))
+
+ score_terms['lamps'] = lamps.sum(lambda l: (
+ cl.center_stable_surface_dist(l.related_to(sidetable)).minimize(weight=1) +
+ l.distance(lamps).maximize(weight=1)
+ ))
+ #endregion
+
+ # region CLOSETS
+ closets = rooms[Semantics.Closet].excludes(cu.room_types)
+ constraints['closets'] = closets.all(lambda r: (
+ (storage.related_to(r).count() >= 1) *
+ ceillights.related_to(r, cu.hanging).count().in_range(0, 1) *
+ (walldec.related_to(r).count() == 0) # special case exclusion - no paintings etc in closets
+ ))
+ score_terms['closets'] = closets.all(lambda r: (
+ storage.related_to(r).count().maximize(weight=2) *
+ obj.related_to(storage.related_to(r)).count().maximize(weight=2)
+ ))
+
+ # NOTE: closets also have special-case behavior below depending on what room they are adjacent to
+ # endregion
+
+ #region BEDROOMS
+ bedrooms = rooms[Semantics.Bedroom].excludes(cu.room_types)
+ beds = wallfurn[Semantics.Bed][seating.BedFactory]
+ constraints['bedroom'] = bedrooms.all(lambda r: (
+
+ beds.related_to(r).count().in_range(1, 2) *
+
+ (
+ sidetable.related_to(r)
+ .related_to(beds.related_to(r), cu.leftright_leftright)
+ .count().in_range(0, 2)
+ ) *
+
+ rugs.related_to(r).count().in_range(0, 2) *
+
+ desks.related_to(r).count().in_range(0, 1) *
+ storage.related_to(r).count().in_range(2, 5) *
+
+ floor_lamps.related_to(r).count().in_range(0, 1) *
+
+ storage.related_to(r).all(lambda s: (
+ (obj[Semantics.OfficeShelfItem].related_to(s, cu.on).count() >= 0)
+ ))
+ ))
+
+ score_terms['bedroom'] = bedrooms.sum(lambda r: (
+ beds.related_to(r).count().maximize(weight=3) +
+ beds.related_to(r).sum(lambda t: cl.distance(r, doors)).maximize(weight=0.5) +
+ sidetable.related_to(r).sum(lambda t: t.distance(beds.related_to(r))).minimize(weight=3)
+ ))
+
+ #endregion
+
+ #region KITCHENS
+ kitchens = rooms[Semantics.Kitchen].excludes(cu.room_types)
+
+ countertops = furniture[Semantics.KitchenCounter]
+ wallcounter = countertops[shelves.KitchenSpaceFactory].related_to(rooms, cu.against_wall)
+ island = countertops[shelves.KitchenIslandFactory]
+ barchairs = furniture[seating.BarChairFactory]
+
+ constraints['kitchen_counters'] = kitchens.all(lambda r: (
+ wallcounter.related_to(r).count().in_range(1, 2) *
+ island.related_to(r).count().in_range(0, 1)
+ ))
+
+ if params['has_kitchen_barstools']:
+ constraints['kitchen_barchairs'] = kitchens.all(lambda r: (
+ barchairs.related_to(island.related_to(r), cu.front_against).count().in_range(0, 4)
+ ))
+
+ score_terms['kitchen_counters'] = kitchens.sum(lambda r: (
+
+ # try to fill 40-60% of kitchen floorplan with countertops (additive with typical furniture incentive)
+ (
+ countertops.related_to(r).volume(dims=2)
+ / r.volume(dims=2).clamp_min(1) # avoid div by 0
+ ).hinge(0.4, 0.6).minimize(weight=10) +
+
+ # cluster countertops together
+ countertops.related_to(r).sum(
+ lambda c: countertops.related_to(r).mean(lambda c2:
+ c.distance(c2)
+ )
+ ).minimize(weight=3)
+
+ ))
+
+ constraints['kitchen_island_placement'] = kitchens.all(lambda r:
+ wallcounter.related_to(r).all(lambda t: (
+ t.distance(island.related_to(r)).in_range(0.7, 3)
+ )) *
+ island.related_to(r).all(lambda t: (
+ t.distance(wallcounter.related_to(r)).in_range(0.7, 3) *
+ (t.distance(r, cu.walltags) > 2)
+ ))
+ )
+
+ score_terms['kitchen_island_placement'] = kitchens.sum(lambda r: (
+ island.sum(lambda t: (
+ cl.angle_alignment_cost(t, wallcounter) +
+ cl.angle_alignment_cost(t, r, cu.walltags)
+ )).minimize(weight=1) +
+ island.distance(r, cu.walltags).hinge(3, 1e7).minimize(weight=10) +
+ wallcounter.sum(lambda t:
+ cl.focus_score(t, island.related_to(r)).minimize(weight=5)
+ )
+ ))
+
+ sink_flush_on_counter = cl.StableAgainst(cu.bottom, {Subpart.SupportSurface}, margin=0.001)
+ sink_against_wall = cl.StableAgainst(cu.back, cu.walltags, margin=0.1)
+ kitchen_sink = (
+ obj[Semantics.Sink][table_decorations.SinkFactory]
+ .related_to(countertops, sink_flush_on_counter)
+ )
+ constraints['kitchen_sink'] = kitchens.all(lambda r: (
+
+ # those sinks can be on either type of counter
+ kitchen_sink.related_to(wallcounter.related_to(r)).count().in_range(0, 1)
+ * kitchen_sink.related_to(island.related_to(r)).count().in_range(0, 1) # island sinks dont need to be against wall
+
+ * countertops.related_to(r).all(lambda c: (
+ kitchen_sink.related_to(c).all(
+ lambda s: s.distance(c, cu.side).in_range(0.05, 0.2)
+ )
+ ))
+ ))
+
+ score_terms['kitchen_sink'] = kitchens.sum(lambda r: (
+
+ countertops.sum(lambda c: kitchen_sink.related_to(c).sum(lambda s: (
+ (s.volume(dims=2) / c.volume(dims=2)).hinge(0.2, 0.4).minimize(weight=10)
+ )))
+
+ + island.related_to(r).sum(lambda isl:( # sinks on islands must be near to edge and oriented outwards
+ kitchen_sink.related_to(isl).sum(lambda s: (
+ cl.angle_alignment_cost(s, isl, cu.side).minimize(weight=10)
+ + cl.distance(s, isl, cu.side).hinge(0.05, 0.07).minimize(weight=10)
+ ))
+ ))
+
+ ))
+
+ kitchen_appliances = obj[Semantics.KitchenAppliance]
+ kitchen_appliances_big = kitchen_appliances.related_to(kitchens, cu.on_floor).related_to(kitchens, cu.against_wall)
+ microwaves = kitchen_appliances[appliances.MicrowaveFactory].related_to(wallcounter, cu.on)
+
+ constraints['kitchen_appliance'] = kitchens.all(lambda r: (
+
+ kitchen_appliances_big[appliances.DishwasherFactory].related_to(r).count().in_range(0, 1)
+ * kitchen_appliances_big[appliances.BeverageFridgeFactory].related_to(r).count().in_range(0, 1)
+ * (kitchen_appliances_big[appliances.OvenFactory].related_to(r).count() == 1)
+
+ * (wallfurn[shelves.KitchenCabinetFactory].related_to(r).count() >= 0)
+
+ * (microwaves.related_to(wallcounter.related_to(r)).count().in_range(0, 1))
+ ))
+
+ score_terms['kitchen_appliance'] = kitchens.sum(lambda r: (
+ kitchen_appliances.sum(lambda t: (
+ t.distance(wallcounter.related_to(r)).minimize(weight=1)
+ + cl.accessibility_cost(t, r, dist=1).minimize(weight=10)
+ + cl.accessibility_cost(t, furniture.related_to(r), dist=1).minimize(weight=10)
+ + t.distance(island.related_to(r)).hinge(0.7, 1e7).minimize(weight=10)
+ ))
+ ))
+
+ obj_on_counter = lambda r: obj.related_to(countertops.related_to(r), cu.on)
+ constraints['kitchen_objects'] = kitchens.all(lambda r: (
+
+ (obj_on_counter(r)[Semantics.KitchenCounterItem].count() >= 0)
+
+ * (obj[Semantics.FoodPantryItem].related_to(storage.related_to(r), cu.on).count() >= 0)
+
+ * island.related_to(r).all(lambda t: (
+ obj[Semantics.TableDisplayItem].related_to(t, cu.ontop).count().in_range(0, 4)
+ ))
+ ))
+
+ score_terms['kitchen_objects'] = kitchens.sum(lambda r: (
+ (
+ obj.related_to(wallcounter, cu.on)
+ .sum(lambda t: t.distance(r, cu.walltags))
+ .minimize(weight=3)
+ )
+ + cl.center_stable_surface_dist(
+ obj.related_to(island.related_to(r), cu.ontop)
+ ).minimize(weight=1)
+ ))
+
+ # disabled for now bc tertiary
+ #constraints['kitchen_appliance_objects'] = kitchens.all(lambda r: (
+ # wallfurn[appliances.DishwasherFactory].related_to(r).all(lambda r: (
+ # (obj[Semantics.Cookware].related_to(r, cu.on).count() >= 0) *
+ # (obj[Semantics.Dishware].related_to(r, cu.on).count() >= 0
+ # )) *
+ # wallfurn[appliances.OvenFactory].related_to(r).all(lambda r: (
+ # (obj[Semantics.Cookware].related_to(r, cu.on).count() >= 0)
+ # ))
+ #)))
+
+ closet_kitchen = closets.related_to(kitchens, cl.RoomNeighbour())
+ constraints['closet_kitchen'] = closet_kitchen.all(lambda r: (
+ obj[Semantics.FoodPantryItem].related_to(storage.related_to(r), cu.on).count() >= 0
+ ))
+ score_terms['closet_kitchen'] = closet_kitchen.sum(lambda r: (
+ storage.related_to(r).count().maximize(weight=2) +
+ obj[Semantics.FoodPantryItem].related_to(storage.related_to(r), cu.on).count().maximize(weight=5)
+ ))
+
+ #score_terms['kitchen_table'] # todo diningtable or hightop
+
+ #endregion
+
+ #region LIVINGROOMS
+
+ livingrooms = rooms[Semantics.LivingRoom].excludes(cu.room_types)
+ sofas = furniture[seating.SofaFactory]
+ tvstands = wallfurn[shelves.TVStandFactory]
+ coffeetables = furniture[tables.CoffeeTableFactory]
+
+ sofa_back_near_wall = cl.StableAgainst(cu.back, cu.walltags, margin=uniform(0.1, 0.3))
+ sofa_side_near_wall = cl.StableAgainst(cu.side, cu.walltags, margin=uniform(0.1, 0.3))
+ freestanding = lambda o, r: (
+ o
+ .related_to(r)
+ .related_to(r, -sofa_back_near_wall)
+ #.related_to(r, -cu.side_against_wall)
+ )
+
+ constraints['sofa'] = livingrooms.all(lambda r: (
+ #sofas.related_to(r).count().in_range(2, 3)
+ sofas.related_to(r, sofa_back_near_wall).count().in_range(2, 4)
+ #* sofas.related_to(r, sofa_side_near_wall).count().in_range(0, 1)
+
+ * freestanding(sofas, r).all(lambda t: ( # frustrum infront of freestanding sofa must directly contain tvstand
+ cl.accessibility_cost(t, tvstands.related_to(r), dist=3) > 0.7
+ ))
+
+ * sofas.all(lambda t: (
+ cl.accessibility_cost(t, furniture.related_to(r), dist=2).in_range(0, 0.5)
+ * cl.accessibility_cost(t, r, dist=1).in_range(0, 0.5)
+ ))
+
+ #* ( # allow a storage object behind non-wall sofas
+ # storage.related_to(r)
+ # .related_to(freestanding(sofas, r))
+ # .count().in_range(0, 1)
+ #)
+ ))
+
+ constraints['sofa_positioning'] = rooms.all(lambda r: (sofas.all(lambda s: (
+ (cl.accessibility_cost(s, rooms, dist=3) < 0.5)
+ * (cl.focus_score(s, tvstands.related_to(r)) > 0.5) # must face or perpendicular to TVStand
+ ))))
+
+ score_terms['sofa'] = livingrooms.sum(lambda r: (
+
+ sofas.volume().maximize(weight=10)
+
+ + sofas.related_to(r).sum(lambda t: (
+
+ t.distance(sofas.related_to(r)).hinge(0, 1).minimize(weight=1)
+ + t.distance(tvstands.related_to(r)).hinge(2, 3).minimize(weight=5)
+
+ + cl.focus_score(t, tvstands.related_to(r)).maximize(weight=5)
+ + cl.angle_alignment_cost(t, tvstands.related_to(r), cu.front).minimize(weight=1)
+ + cl.focus_score(t, coffeetables.related_to(r)).maximize(weight=2)
+
+ + cl.accessibility_cost(t, r, dist=3).minimize(weight=3)
+ ))
+
+ + freestanding(sofas, r).sum(lambda t: (
+ cl.angle_alignment_cost(t, tvstands.related_to(r)).minimize(weight=5)
+ + cl.angle_alignment_cost(t, r, cu.walltags).minimize(weight=3)
+ + cl.center_stable_surface_dist(t).minimize(weight=0.5)
+ ))
+ ))
+
+ tvs = obj[appliances.TVFactory].related_to(tvstands, cu.ontop)
+
+ if params['has_tv']:
+ constraints['tv'] = livingrooms.all(lambda r: (
+ tvstands.related_to(r).all(lambda t: (
+ (tvs.related_to(t).count() == 1)
+
+ * tvs.related_to(t).all(lambda tv:
+ cl.accessibility_cost(tv, r, dist=1).in_range(0, 0.1)
+ )
+ ))
+ ))
+
+ score_terms['tvstand'] = rooms.all(lambda r: (tvstands.sum(lambda stand: (
+ tvs.related_to(stand).volume().maximize(weight=1)
+
+ + stand.distance(window).maximize(weight=1) # penalize being very close to window. avoids tv blocking window.
+ + cl.accessibility_cost(stand, furniture).minimize(weight=3)
+
+ + cl.center_stable_surface_dist(stand).minimize(weight=5) # center tvstand against wall (also tries to do vertical & floor but those are constrained)
+ + cl.center_stable_surface_dist(tvs.related_to(stand)).minimize(weight=1)
+ ))))
+
+ constraints['livingroom'] = livingrooms.all(lambda r: (
+ storage.related_to(r).count().in_range(1, 5)
+
+ * tvstands.related_to(r).count().equals(1)
+
+ * ( # allow sidetables next to any sofa
+ sidetable.related_to(r)
+ .related_to(sofas.related_to(r), cu.side_by_side)
+ .count().in_range(0, 2)
+ )
+
+ * desks.related_to(r).count().in_range(0, 1)
+ * coffeetables.related_to(r).count().in_range(0, 1)
+ * coffeetables.related_to(r).all(lambda t: (
+ (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count().in_range(0, 3))
+ ))
+
+ * (
+ rugs
+ .related_to(r)
+ #.related_to(furniture.related_to(r), cu.side_by_side)
+ .count().in_range(0, 2)
+ )
+ ))
+
+ score_terms['livingroom'] = livingrooms.sum(lambda r: (
+
+ coffeetables.related_to(r).sum(lambda t: (
+
+ # ideal coffeetable-to-tv distance according to google
+ t.distance(sofas.related_to(r)).hinge(0.45, 0.6).minimize(weight=5)
+
+ + cl.angle_alignment_cost(t, sofas.related_to(r), cu.front).minimize(weight=5)
+ + cl.focus_score(sofas.related_to(r), t).maximize(weight=5)
+ ))
+ ))
+
+
+ constraints['livingroom_objects'] = livingrooms.all(lambda r: (
+ storage.all(lambda t: (
+ (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count() >= 0)
+ )) *
+ coffeetables.all(lambda t: (
+ obj[Semantics.TableDisplayItem].related_to(t, cu.ontop).count().in_range(0, 1) *
+ (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count() >= 0)
+ ))
+ ))
+
+ #endregion
+
+ #region DININGROOMS
+
+ diningtables = furniture[Semantics.Table][tables.TableDiningFactory]
+ diningchairs = furniture[Semantics.Chair][seating.ChairFactory]
+ constraints['dining_chairs'] = rooms.all(lambda r: (
+ diningtables.related_to(r).all(lambda t: (
+ diningchairs.related_to(r).related_to(t, cu.front_against).count().in_range(3, 6)
+ ))
+ ))
+
+ score_terms['dining_chairs'] = rooms.all(lambda r: (
+ diningchairs.related_to(r).count().maximize(weight=5) +
+ diningchairs.related_to(r).sum(lambda t: t.distance(diningchairs.related_to(r))).maximize(weight=3)
+ #cl.reflectional_asymmetry(diningchairs.related_to(r), diningtables.related_to(r)).minimize(weight=1)
+ #cl.rotational_asymmetry(diningchairs.related_to(r)).minimize(weight=1)
+ ))
+
+ constraints['dining_table_objects'] = rooms.all(lambda r: (
+ diningtables.related_to(r).all(lambda t: (
+ obj[Semantics.TableDisplayItem].related_to(t, cu.ontop).count().in_range(0, 2) *
+ (obj[Semantics.Utensils].related_to(t, cu.ontop).count() >= 0) *
+ (obj[Semantics.Dishware].related_to(t, cu.ontop).count().in_range(0, 2))
+ ))
+ ))
+
+ score_terms['dining_table_objects'] = rooms.sum(lambda r: (
+ cl.center_stable_surface_dist(
+ obj[Semantics.TableDisplayItem]
+ .related_to(diningtables.related_to(r), cu.ontop)
+ ).minimize(weight=1)
+ ))
+
+ diningrooms = rooms[Semantics.DiningRoom].excludes(cu.room_types)
+ constraints['diningroom'] = diningrooms.all(lambda r: (
+ (diningtables.related_to(r).count() == 1) *
+ storage.related_to(r).all(lambda t: (
+ (obj[Semantics.Dishware].related_to(t, cu.on).count() >= 0) *
+ (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count().in_range(0, 5))
+ ))
+ ))
+ score_terms['diningroom'] = diningrooms.sum(lambda r: (
+
+ diningtables.related_to(r).distance(r, cu.walltags).maximize(weight=10)
+ + cl.angle_alignment_cost(diningtables.related_to(r), r, cu.walltags).minimize(weight=10)
+ + cl.center_stable_surface_dist(diningtables.related_to(r)).minimize(weight=1)
+ ))
+ #endregion
+
+ #region BATHROOMS
+ bathrooms = rooms[Semantics.Bathroom].excludes(cu.room_types)
+ toilet = wallfurn[bathroom.ToiletFactory]
+ bathtub = wallfurn[bathroom.BathtubFactory]
+ sink = wallfurn[bathroom.StandingSinkFactory]
+ hardware = obj[bathroom.HardwareFactory].related_to(bathrooms, cu.against_wall)
+ constraints['bathroom'] = bathrooms.all(lambda r: (
+
+ mirror.related_to(r).related_to(r, cu.flush_wall).count().equals(1) *
+ sink.related_to(r).count().equals(1) *
+ toilet.related_to(r).count().equals(1) *
+
+ storage.related_to(r).all(lambda t: (
+ (obj[Semantics.BathroomItem].related_to(t, cu.on).count() >= 0)
+ ))
+ ))
+
+ score_terms['toilet'] = rooms.all(lambda r: (
+ toilet.distance(doors).maximize(weight=1) +
+ toilet.distance(furniture).maximize(weight=1) +
+ toilet.distance(sink).maximize(weight=1) +
+ cl.accessibility_cost(toilet, furniture, dist=2).minimize(weight=10)
+ ))
+
+ constraints['bathtub'] = bathrooms.all(lambda r: (
+ bathtub.related_to(r).count().in_range(0, 1) *
+ hardware.related_to(r).count().in_range(1, 4)
+ ))
+ score_terms['bathtub'] = bathrooms.all(lambda r: (
+
+ bathtub.sum(lambda t: t.distance(hardware)).minimize(weight=0.2)
+ + sink.sum(lambda t: t.distance(hardware)).minimize(weight=0.2)
+
+ + hardware.sum(lambda t: (
+ t.distance(rooms, cu.floortags)
+ .hinge(0.5, 1)
+ .minimize(weight=15)
+ ))
+
+ ))
+
+ score_terms['bathroom'] = (
+ mirror.related_to(bathrooms).distance(sink).minimize(weight=0.2)
+ + cl.accessibility_cost(mirror, furniture, cu.down_dir).maximize(weight=3)
+ )
+ #endregion
+
+ #region MISC OBJECTS
+
+ if params['has_aquarium_tank']:
+
+ aqtank = lambda r: obj[decor.AquariumTankFactory].related_to(storage.related_to(r), cu.ontop)
+
+ constraints['aquarium_tank'] = (
+ aqtank(rooms).count().in_range(0, 1)
+ )
+ score_terms['aquarium_tank'] = rooms.all(lambda r: (
+ aqtank(r).distance(r, cu.walltags).hinge(0.05, 0.1).minimize(weight=1)
+ ))
+
+ if params['has_birthday_balloons']:
+ balloons = obj[wall_decorations.BalloonFactory].related_to(rooms, cu.against_wall)
+ constraints['birthday_balloons'] = (
+ balloons.related_to(rooms, cu.against_wall).count().in_range(0, 3)
+ )
+ score_terms['birthday_balloons'] = rooms.all(lambda r: (
+ balloons.sum(lambda b: b.distance(r, cu.floortags).hinge(1.6, 2.5).minimize(weight=1))
+ ))
+
+ if params['has_cocktail_tables']:
+
+ cocktail_table = (
+ furniture[tables.TableCocktailFactory]
+ .related_to(rooms, cu.on_floor)
+ .related_to(rooms, cu.against_wall)
+ )
+
+ constraints['cocktail_tables'] = diningrooms.all(lambda r: (
+ cocktail_table.related_to(r).count().in_range(0, 3)
+ *(
+ barchairs.related_to(cocktail_table.related_to(r), cu.front_against)
+ .count().in_range(0, 4)
+ )
+ * (
+ obj[tableware.WineglassFactory]
+ .related_to(cocktail_table.related_to(r), cu.ontop)
+ .count().in_range(0, 4)
+ )
+ ))
+ score_terms['cocktail_tables'] = diningrooms.sum(lambda r: (
+ cocktail_table.related_to(r).sum(lambda t: (
+
+ t.distance(r, cu.walltags).hinge(0.5, 1).minimize(weight=1)
+ + t.distance(cocktail_table.related_to(r)).hinge(1, 2).minimize(weight=1)
+
+ + barchairs.related_to(t).sum(
+ lambda c: c.distance(barchairs.related_to(t))
+ ).maximize(weight=1)
+ ))
+ ))
+
+ #endregion
+
+ return cl.Problem(
+ constraints=constraints,
+ score_terms=score_terms,
+ )
+
+all_constraint_funcs = [
+ home_constraints
+]
diff --git a/infinigen_examples/util/constraint_util.py b/infinigen_examples/util/constraint_util.py
new file mode 100644
index 000000000..5345a586a
--- /dev/null
+++ b/infinigen_examples/util/constraint_util.py
@@ -0,0 +1,63 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import numpy as np
+
+from infinigen.core import tags as t
+from infinigen import assets as a
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ example_solver,
+ usage_lookup
+)
+
+room_types = {
+ t.Semantics.Kitchen,
+ t.Semantics.Bedroom,
+ t.Semantics.LivingRoom,
+ t.Semantics.Closet,
+ t.Semantics.Hallway,
+ t.Semantics.Bathroom,
+ t.Semantics.Garage,
+ t.Semantics.Balcony,
+ t.Semantics.DiningRoom,
+ t.Semantics.Utility,
+ t.Semantics.Staircase,
+}
+
+all_sides = {t.Subpart.Bottom, t.Subpart.Top, t.Subpart.Front, t.Subpart.Back}
+walltags = {t.Subpart.Wall, t.Subpart.Visible, -t.Subpart.SupportSurface, -t.Subpart.Ceiling}
+floortags = {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Wall, -t.Subpart.Ceiling}
+ceilingtags = {t.Subpart.Visible, t.Subpart.Ceiling, -t.Subpart.Wall, -t.Subpart.SupportSurface}
+
+front_dir = np.array([0, 1, 0])
+back_dir = np.array([0, -1, 0])
+down_dir = np.array([0, 0, -1])
+
+bottom = {t.Subpart.Bottom, -t.Subpart.Top, -t.Subpart.Front, -t.Subpart.Back}
+back = {t.Subpart.Back, -t.Subpart.Top, -t.Subpart.Front}
+top = {t.Subpart.Top, -t.Subpart.Back, -t.Subpart.Bottom, -t.Subpart.Front}
+side = {-t.Subpart.Top, -t.Subpart.Bottom, -t.Subpart.Back, -t.Subpart.SupportSurface}
+front = {t.Subpart.Front, -t.Subpart.Top, -t.Subpart.Bottom, -t.Subpart.Back}
+leftright = {-t.Subpart.Top, -t.Subpart.Bottom, -t.Subpart.Back, -t.Subpart.Front, -t.Subpart.SupportSurface}
+
+on_floor = cl.StableAgainst(bottom, floortags, margin=0.01)
+flush_wall = cl.StableAgainst(back, walltags, margin=0.02)
+against_wall = cl.StableAgainst(back, walltags, margin=0.07)
+spaced_wall = cl.StableAgainst(back, walltags, margin=0.8)
+hanging = cl.StableAgainst(top, ceilingtags, margin=0.05)
+side_against_wall = cl.StableAgainst(side, walltags, margin=0.05)
+
+ontop = cl.StableAgainst(bottom, top)
+on = cl.StableAgainst(bottom, {t.Subpart.SupportSurface})
+
+front_against = cl.StableAgainst(front, side, margin=0.05, check_z=False) #check_z=False
+leftright_leftright = cl.StableAgainst(leftright, leftright, margin=0.05)
+side_by_side = cl.StableAgainst(side, side)
+back_to_back = cl.StableAgainst(back, back)
+
+variable_room = t.Variable('room')
+variable_obj = t.Variable('obj')
\ No newline at end of file
diff --git a/infinigen_examples/util/generate_indoors_util.py b/infinigen_examples/util/generate_indoors_util.py
new file mode 100644
index 000000000..ea41810f1
--- /dev/null
+++ b/infinigen_examples/util/generate_indoors_util.py
@@ -0,0 +1,265 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+import logging
+import typing
+
+import bpy
+from mathutils import Vector
+
+import gin
+import numpy as np
+from numpy.random import uniform, normal, randint
+import trimesh
+
+from infinigen.terrain import Terrain, hidden_in_viewport
+from infinigen.terrain.utils import Mesh
+from infinigen.assets.materials import invisible_to_camera
+
+from infinigen.core.constraints import (
+ constraint_language as cl,
+ reasoning as r,
+ usage_lookup
+)
+
+from infinigen.assets import weather
+from infinigen.assets.scatters import grass, pebbles
+from infinigen.core.placement import density, split_in_view
+from infinigen.core.util import (blender as butil, pipeline)
+from infinigen.core.util.camera import points_inview
+from infinigen.core import tags as t
+
+from . import constraint_util as cu
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+def within_bbox_2d(verts, bbox):
+ return (
+ (verts[:, 0] > bbox[0][0]) &
+ (verts[:, 0] < bbox[1][0]) &
+ (verts[:, 1] > bbox[0][1]) &
+ (verts[:, 1] < bbox[1][1])
+ )
+
+def create_outdoor_backdrop(
+ terrain: Terrain,
+ house_bbox: tuple,
+ cam,
+ p: pipeline.RandomStageExecutor,
+ params: dict
+):
+
+ all_vertices = []
+ for name in terrain.terrain_objs:
+ if name not in hidden_in_viewport:
+ all_vertices.append(Mesh(obj=terrain.terrain_objs[name]).vertices)
+
+ all_vertices = np.concatenate(all_vertices)
+ all_mask = within_bbox_2d(all_vertices, house_bbox)
+
+ if not all_mask.any():
+ height = 0
+ else:
+ height = all_vertices[all_mask, 2].max()
+
+ extra_zoff = uniform(0, 4) # deliberately float above the terrain.
+ height += extra_zoff
+
+ for obj in terrain.terrain_objs.values():
+ obj.location[2] -= height
+ butil.apply_transform(obj, loc=True)
+
+ main_terrain = bpy.data.objects['OpaqueTerrain']
+ verts = np.zeros(3 * len(main_terrain.data.vertices), float)
+ main_terrain.data.vertices.foreach_get('co', verts)
+ verts = verts.reshape(-1, 3)
+ mask = within_bbox_2d(verts, house_bbox)
+
+ with butil.ViewportMode(main_terrain, mode='EDIT'):
+ bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ split_in_view.select_vertmask(main_terrain, mask)
+ with butil.ViewportMode(main_terrain, mode='EDIT'):
+ bpy.ops.mesh.select_more()
+ bpy.ops.mesh.delete(type='VERT')
+
+ p.run_stage('fancy_clouds', weather.kole_clouds.add_kole_clouds)
+
+ terrain_inview, *_ = split_in_view.split_inview(main_terrain, verbose=True, outofview=False,
+ print_areas=True, cam=cam, vis_margin=2, dist_max=params['near_distance'], hide_render=True,
+ suffix='inview')
+
+ def add_grass(target):
+ select_max = params.get('grass_select_max', 0.5)
+ selection = density.placement_mask(normal_dir=(0, 0, 1), scale=0.1, return_scalar=True,
+ select_thresh=uniform(select_max / 2, select_max))
+ grass.apply(target, selection=selection)
+
+ p.run_stage('grass', add_grass, terrain_inview)
+
+ def add_rocks(target):
+ selection = density.placement_mask(scale=0.15, select_thresh=0.5, normal_thresh=0.7, return_scalar=True)
+ _, rock_col = pebbles.apply(target, selection=selection)
+ return rock_col
+
+ p.run_stage('rocks', add_rocks, terrain_inview)
+ return height
+
+def place_cam_overhead(cam: bpy.types.Object, bbox: tuple[np.array]):
+
+ butil.spawn_point_cloud('place_cam_overhead', bbox)
+
+ mins, maxs = bbox
+ cam.location = (maxs + mins) / 2
+ cam.rotation_euler = (0, 0, 0)
+ for cam_dist in np.exp(np.linspace(-1., 5.5, 500)):
+ cam.location[-1] = cam_dist
+ bpy.context.view_layer.update()
+ inview = points_inview(bbox, cam.children[0])
+ if inview.all():
+ for area in bpy.context.screen.areas:
+ if area.type == 'VIEW_3D':
+ area.spaces.active.region_3d.view_perspective = 'CAMERA'
+ break
+ return
+
+
+def overhead_view(cam, room_name):
+ room_name = room_name.split('.')[0]
+
+ for o in bpy.data.objects:
+ if '.exterior' in o.name:
+ o.hide_viewport = True
+ o.hide_render = True
+ elif '.ceiling' in o.name:
+ invisible_to_camera.apply(o)
+
+ floor = bpy.data.objects[room_name + '.floor']
+ cam.location = floor.location + Vector((0, 0, 10))
+ cam.rotation_euler = (0, 0, 0)
+
+def hide_other_rooms(state, rooms_split, keep_rooms: list[str]):
+
+ for col in rooms_split.values():
+ for o in col.objects:
+ if any(
+ roomname.split('.')[0] in o.name
+ for roomname in keep_rooms
+ ):
+ continue
+ o.hide_viewport = True
+ o.hide_render = True
+
+ hide_cutters = [
+ o
+ for k, os in state.objs.items()
+ if t.Semantics.Cutter in os.tags and not any(
+ rel.target_name == roomname
+ for rel in os.relations
+ for roomname in keep_rooms
+ )
+ for o in butil.iter_object_tree(os.obj)
+ ]
+ for o in hide_cutters:
+ o.hide_render = True
+ o.hide_viewport = True
+ bpy.context.scene.render.film_transparent = True
+
+def apply_greedy_restriction(
+ stages: dict[str, r.Domain],
+ filter_tags: set[str],
+ var: t.Variable,
+ scope_domain: r.Domain = None,
+):
+ filter_tags = t.to_tag_set(filter_tags, fac_context=usage_lookup._factory_lookup)
+ for k, d in stages.items():
+ if scope_domain is not None and not d.intersects(scope_domain):
+ continue
+ stages[k], match = r.domain_tag_substitute(d, var, r.Domain(filter_tags).with_tags(var), return_match=True)
+ logger.info(f'{apply_greedy_restriction.__name__} restricting {k=} to {filter_tags=} for {var=}')
+
+@gin.configurable
+def restrict_solving(
+ stages: dict[str, r.Domain],
+ problem: cl.Problem,
+
+ # typically provided by gin
+ restrict_parent_rooms: set[str] = None,
+ restrict_parent_objs: set[str] = None,
+ restrict_child_primary: set[str] = None,
+ restrict_child_secondary: set[str] = None,
+ solve_max_rooms: int = None,
+ solve_max_parent_obj: int = None,
+ consgraph_filters: typing.Iterable[str] = None,
+):
+
+ """Restricts solving to a subset of the full house or constraint graph.
+
+ Parameters
+ ----------
+ stages : the original set of greedy stages
+ problem : the original constraint specification
+ restrict_parent_rooms : limit solving to only use these rooms as parent rooms
+ restrict_parent_objs : limit solving to only use these objects as parent objects
+ restrict_child_primary : limit solving to only place primary objects of these types (e.g, only place diningtables, no shelves etc)
+ restrict_child_secondary : if specified, limit solving to only place secondary objects of these types (e.g only place mugs, no plates etc)
+ solve_max_rooms : only place objects in at most this many rooms (e.g.. only 1 room has objects in it)
+ solve_max_parent_obj : only place objects onto at most this many parent objects (e.g. only 1 shelf has objects on it)
+
+ Returns
+ -------
+ stages : the modified set of greedy stages
+ problem : the modified constraint specification
+ limits : set of object-quantity-limits for solving
+
+ """
+
+ obj_domain = r.Domain({t.Semantics.Object})
+ primary_obj_domain = r.Domain({t.Semantics.Object}, [(-cl.AnyRelation(), obj_domain)])
+ secondary_obj_domain = r.Domain({t.Semantics.Object}, [(cl.AnyRelation(), obj_domain)])
+
+ if restrict_parent_rooms is not None:
+ apply_greedy_restriction(stages, restrict_parent_rooms, cu.variable_room)
+
+ if restrict_parent_objs is not None:
+ apply_greedy_restriction(stages, restrict_parent_objs, cu.variable_obj)
+
+ if restrict_child_primary is not None:
+ restrict_child_primary = t.to_tag_set(restrict_child_primary, fac_context=usage_lookup._factory_lookup)
+ for k, d in stages.items():
+ if d.intersects(primary_obj_domain):
+ logger.info(f'restrict_solving applying restrict_child_primary, limiting {k} to objects satisfying {restrict_child_primary}')
+ stages[k] = d.intersection(r.Domain(restrict_child_primary))
+
+ if restrict_child_secondary is not None:
+ restrict_child_secondary = t.to_tag_set(restrict_child_secondary, fac_context=usage_lookup._factory_lookup)
+ for k, d in stages.items():
+ if d.intersects(secondary_obj_domain):
+ logger.info(f'restrict_solving applying restrict_child_secondary, limiting {k} to objects satisfying {restrict_child_primary}')
+ stages[k] = d.intersection(r.Domain(restrict_child_secondary))
+
+ quantity_limits = {
+ cu.variable_room: solve_max_rooms,
+ cu.variable_obj: solve_max_parent_obj
+ }
+
+ if consgraph_filters is not None:
+ if isinstance(consgraph_filters, str):
+ consgraph_filters = [consgraph_filters]
+ assert isinstance(consgraph_filters, typing.Iterable)
+ old_counts = (len(problem.constraints), len(problem.score_terms))
+
+ filter = lambda d: {
+ k: v for k, v in d.items()
+ if any(fi in k for fi in consgraph_filters)
+ }
+ problem = cl.Problem(filter(problem.constraints), filter(problem.score_terms))
+
+ new_counts = (len(problem.constraints), len(problem.score_terms))
+ logger.info(f'restrict_solving filtered consgraph from {old_counts=} {new_counts=} using {consgraph_filters=}')
+
+ return stages, problem, quantity_limits
\ No newline at end of file
diff --git a/infinigen_examples/util/test_utils.py b/infinigen_examples/util/test_utils.py
new file mode 100644
index 000000000..2892bd7f4
--- /dev/null
+++ b/infinigen_examples/util/test_utils.py
@@ -0,0 +1,60 @@
+# Copyright (c) Princeton University.
+# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory
+# of this source tree.
+
+# Authors: Alexander Raistrick
+
+from pathlib import Path
+import importlib
+import pdb
+
+import gin
+import bpy
+
+from infinigen.core import surface
+from infinigen.core.util import blender as butil, math as mutil
+from infinigen.core import init
+from infinigen.core.constraints.example_solver.room import constants
+
+def setup_gin(configs_folder, configs=None, overrides=None):
+ gin.clear_config()
+ init.apply_gin_configs(
+ configs_folder=Path(configs_folder),
+ configs=configs,
+ overrides=overrides,
+ skip_unknown=True,
+ finalize_config=False
+ )
+ surface.registry.initialize_from_gin()
+ gin.unlock_config()
+
+ with mutil.FixedSeed(0):
+ constants.initialize_constants()
+
+
+def import_item(name):
+ *path_parts, name = name.split('.')
+ with gin.unlock_config():
+
+ try:
+ return importlib.import_module('.' + name, '.'.join(path_parts))
+ except ModuleNotFoundError:
+ mod = importlib.import_module('.'.join(path_parts))
+ return getattr(mod, name)
+
+def load_txt_list(path: Path, skip_sharp=True):
+
+ path = Path(path)
+ pathabs = path.absolute()
+
+ if not pathabs.exists():
+ raise FileNotFoundError(f'{path=} resolved to {pathabs=} which does not exist')
+
+ res = pathabs.read_text().splitlines()
+ res = [
+ f.lstrip('#').lstrip(' ')
+ for f in res if
+ (not f.startswith('#') or not skip_sharp)
+ and len(f) > 0
+ ]
+ return res
diff --git a/log.txt b/log.txt
new file mode 100644
index 000000000..517b8103e
--- /dev/null
+++ b/log.txt
@@ -0,0 +1,18262 @@
+commit a2880ed6024ee66476a7c5f0311973e1a368f8f2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 3 lines to infinigen/datagen/configs/export.gin. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit 5799d04378407f7c38e4c4e14feca96057d74042
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 10 lines to infinigen/datagen/configs/indoor_background_configs.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit d8d29f06702a503c687e78c424688b59ed7e06c5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 2 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 967cd920797cefda6946ebea9a4b712aab5d12d2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 15 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit a53846ff5697191fdf31b1e867e577173f7757d4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 97 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ad7e685d457575dea212ae9db19258176adab396
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 1 lines to infinigen/assets/decor/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit d6d5db4594fa201ad6f96ad577a219f40bf31505
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 6 lines to infinigen/assets/windows/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit b95f42aeb0e432f8fb1c85aab97088caa011e777
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 2 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit b0ad83e089ae92a288d2f19b4b4489e5dd9985ff
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 51 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 290ad43054732d51b487dd581b9742f266fc8460
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 203 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 4926ed7f93550c14fa1b724d62e417381226dd33
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 779 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit d7a4cbc3fadc990f956fcd2692d2c344344072f0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 17 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit 30346ae3a72e8ef32782706d4418c8b89e7f31af
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 17 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 661a2f937ce44577b2593aaacd75766f949b6f96
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 181 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 507bc958c7cd167e4f03374b7420841c6a107d7c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 324 lines to infinigen/assets/table_decorations/utils.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 9d797df96982c2c7c46c5bf19afab6f52f25d34e
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 43158158b74af7bbff2ea966d4068eb98a5088c4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 31e053fde4b582ab8b006f505abcc4237785e60e
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 82659a743d753ef34fa6aaed5795a407653d898f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:33 2024 -0700
+
+ Add 1 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 4ce599f72d31b21593eeb761dcbc93f06ea88e54
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 4 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 6d029f33e4fc8632abd71c74e10ced13ac2b0e06
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 31 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 251c3323b477bd46c1b76097bf1a86d0422103e8
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 67 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 3673bd9addb86ef19ba616a39f441e065ffa43ec
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 198 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 17eeb5ea4609c23d99c571bed2a530aa04fcecef
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 5 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 22e4bab7ccd66079ba856c2da5841f7f42e95c88
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 62 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit bfffcc718eebcb71abbac3f40f98e3cd6f96bb26
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 102 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 32fe356161b1211a2ea21683ce23a4c386c200c5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 117 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 232d51bf497c34f104cf76ad251b0755d37b95f8
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 225 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ac425f57ecce19a0926d6bc538d9c454e161720c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 274 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit dc577759538978d4f37368891d05e455bc705727
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 4 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit b796a28d2e4f14864dffe37effec2cf949d055f2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 46 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit db4a31eefb74d73ccf38ade5240b0798746bdb5c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 100 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 82d691ba20db24fa23305aa0a345ba6ab0c45a2c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 2 lines to infinigen/assets/seating/chairs/seats/round_seats.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit c1be072ff44524bc56caff58e0d57586a77713ee
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 39 lines to infinigen/assets/seating/chairs/seats/round_seats.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 81121be9907b9889d5d5edf78e220ac4d1f5cb2d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 1 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit efb0781287d01576d117fa0ec0eea312138a41ab
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 3 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit f7ae6f6ebcc24fc39778347c063e05fea35b5356
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 15 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 79e8a8bfe40545f30724f61f0435932211f95d3b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 37 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit eb3bcead798cdcac5725f31c9b6a2fea34a79b3b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 117 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit cc312dba6416a9cd829367857a6e6bb5cfdea149
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 7 lines to infinigen/assets/seating/chairs/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 742a9bde5bddd1c98ea68a5e602a38c49510dfd5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 14 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 5c0f875d7464aa157de1a5787d85c4e92c1078e7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 22 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit b67001ffdabc22106fe1ee46d8a3d64c7f87ba12
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 328 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit e2eadbee16f7f1855316366686016de432fe824a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 12 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit bccbfc7c07d51e631bca4ce2ffdfcbc7619cade1
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 18 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 3ad3ba7cd71652e5922bc93a6b6cc6d4b16d80aa
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 36 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 8d379da567082d5220c684de7962dc3bca611a20
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 140 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 2cd5fde810f6d246009ec3d48d20518cbd0945df
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 2 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 9c7e927d6fab7ab3cd40829f851bccf6707c96bf
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 4 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 51386633d851a40f67c86f62f518ee8979817db2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 18 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit f1092c7d6439f1111e2a000c9fac8730ff9ba59c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:32 2024 -0700
+
+ Add 75 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 24897d944f697f4b736c02a0e2a276bce3391af4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 239 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit d291d57d0b58d4d2f602ce9fb56c003f7ee45a6a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 405 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit 44492af609664e71269a3338b0da394c805c287f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 7 lines to infinigen/assets/seating/bedframe.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 6c3e78b9f21c7667b29ea6dcd7f08b449f82add1
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 172 lines to infinigen/assets/seating/bedframe.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 82e3bbd29d6ecb4b7cc7859bc24006211efd3a1e
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 186 lines to infinigen/assets/seating/bed.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 719d71574366e13b1f25a79418e70be51879d4cc
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 2 lines to infinigen/assets/seating/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit c27b36c6e528ab28e3659de402be9c67fe844550
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 9 lines to infinigen/assets/seating/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 96be2a15d86fd42917373414bd56ed5f0e59bcb2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 5 lines to infinigen/assets/seating/pillow.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 7476bb3d477591dbea2bc7fc31b5a7432de78a82
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 112 lines to infinigen/assets/seating/pillow.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 418830e3d2c43a2e53d1e628ea1ab687ef67e5a7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 3 lines to infinigen/assets/seating/mattress.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 04a797719f28c0b9d60524bdefb990fed2764c1b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 123 lines to infinigen/assets/seating/mattress.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit cc44ca40188a29613fd3d6041fe09e514fee142d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 34 lines to infinigen/assets/lighting/holdout_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 05656cb0c4352348ba67cca3eaeb102a4ba37ed7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 33 lines to infinigen/assets/lighting/hdri_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit eecce753d2dfb0c306cce5734a350bbf563f8e7a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 3 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 20593ad4182a4e801d38af2e0c5022ddfce68364
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 34 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit a18083caf2d0a02471fef3336c47e218f891aa00
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 35 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 2fd8fff7ffce0249105e768e63a3a7f9c8bc6d7d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 145 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 7e964a5b0c97c18c3544c98bc8a99f528a917937
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 1 lines to infinigen/assets/lighting/three_point_lighting.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit ca322453ecacbe8af3283847a356fad21c57c2bd
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 27 lines to infinigen/assets/lighting/three_point_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit f544bfb3893912e3cc1400d415c3ba74148e96d9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 9 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit 48aebf3bb533ae0afc860ae9115f7286ef0485c7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 32 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit a677764a37b3f3767f7298572ce4673a6da32111
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 75 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 0fdcf6b61300031b75a5d77c83c33e10c9af224b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 114 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit c7a2cd3392638d2b4d17c61f367de8c7a90f1e34
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 318 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 6051bb06ff4c07b8ec219fea183f7db3f897d38d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 56 lines to infinigen/assets/lighting/indoor_lights.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 65e5eafc9dcacac32f14e4c7e6852eadff8447d2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 2 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 9ef918e63e9b52c8d9f4e968308068a6aafcce49
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 5 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit d848ff6e9e6499e520464dd39ca327f33cf09518
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 232 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit 46aceff291cf28a1de61b772afebbd8db6a577df
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 9 lines to infinigen/assets/scatters/clothes.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit c3da2edc9d1138d95fe3cc65b0c280c7621e5797
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 69 lines to infinigen/assets/scatters/clothes.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit f0550f9410127ddb84040219c0917cad06b10820
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 1 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit 758f3ffe84f8ec8e746ee1e7db7ff8d333c8e8cc
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 1 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit a64c3bb41482525812294f4e3e9a17d0eee5583c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 16 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 68b4d91eb3cae215eb0e39a4f64e276d41ece306
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 80 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit 9a174c4e5584fea243383c3ce19b2f1bcf6492eb
+Author: pvl-bot
+Date: Sun Jun 16 23:16:31 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/doors/casing.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit 09a38e9b593111d8790e266485174aebfb2b3fcd
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 58 lines to infinigen/assets/elements/doors/casing.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit dfbf582c656f7c11976b9bd77079f49afdc91928
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit e823115f6b324a16775df3cc563d9d3c83ccbf80
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 5 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit 67cb84726faf578bd2c415376dfe5dcfd08289ae
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 208 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit f40a580f07936e4ef7a317f04cf6d80d4bf983e3
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 92 lines to infinigen/assets/elements/doors/panel.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit b988978d154121e172003b9443f2864f8e141ce2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 55 lines to infinigen/assets/elements/doors/lite.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 18592ec491d767ed7d0a1f190e5750db5ccf414a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 80 lines to infinigen/assets/elements/doors/louver.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 664c4a4d48c23840bbb4abfc5363610097a541b0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 33 lines to infinigen/assets/elements/doors/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 23295ac304f69ba0e02409b51a5f3aceda16e26f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 3 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit ee6412ee2b226388442f382ddec68a975c930ac7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit 644c10af55d2ef92db16bfeed1b763bf7fd244ef
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 55 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit b635d284d52ab1f1805a8abf37f720288aff248e
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 3 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit e188fc64f29830bfd155b7323f7f19e113d3db7d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit c0bcbe19e692117b9b3ee5be25066972b51a6c3a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 60 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit e7605ec909ac5ccf26f085cd0bfa21b6a36edf8d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 3 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 79d2823a8ce6b20259cd0c026028873d93023998
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit 01ecc4b07a8f014ff1539dfb12986d2450a38b93
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 5 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 9edd3de55126c59dd961725c75fb669276697856
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 592 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 65e63943c4c7477ffe866263c9846ee0c7951396
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 32 lines to infinigen/assets/elements/staircases/cantilever.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 662d39f85213136ab71d588ee9bfdf653bcf274d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 18 lines to infinigen/assets/elements/staircases/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit c8217b58eadc8960102fa3b92d07bdf1a35dd1e4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 145 lines to infinigen/assets/elements/staircases/l_shaped.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ebb5aad01803bcbe8226094b31e5ee999e30468d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 33 lines to infinigen/assets/elements/staircases/generate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit a21b6e90d804c65eaccc64aff5f0c7d3659b5833
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 3 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 0e6c023542651027401aa2715c84e86c08030d31
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 4 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit d0dbb0e43aefb3c4eba6b03bf2fa255fc960279a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 151 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 45e790e917ebafe2a640f9d42c551fc64ca38ab7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 6 lines to infinigen/assets/elements/warehouses/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 6095f72899b84b5ad800a7c18d19a467d4f2fa5d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 2 lines to infinigen/assets/elements/warehouses/pallet.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit d73fa6d20268beeb8c4c0c8149da937d9e05c0fa
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 85 lines to infinigen/assets/elements/warehouses/pallet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 403907546b33290bc72d3bb856b9a3c411695a56
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 2 lines to infinigen/assets/elements/warehouses/rack.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit b239d307e18bf464a8eca2bbe9c95dbb5899eb5f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 166 lines to infinigen/assets/elements/warehouses/rack.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 1e34fd0a6657cdd155178275e5f55fd2749b3b2a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 123 lines to infinigen/assets/elements/pillars.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit fe82da6c9595e6cfabb5fdfbffe89b1099ac8a5a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 1 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 04df9e273de641025e9a389a808fb5f0710140c6
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 5 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 5217e1d9d3512f1e36c2ca3f4bd7e70c0cce54d0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:30 2024 -0700
+
+ Add 58 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ea5ee3624aebaaee3d8f02d3d452e36d990dddea
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 1 lines to infinigen/assets/elements/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 20196bdbb1954245ca79a66ed3974510e6a94456
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 9 lines to infinigen/assets/elements/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 8666934337e5f38686a12de490cf394362a85738
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 8 lines to infinigen/assets/utils/uv.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit f3618de2748eebac61457c3fc2a7eb6aa7fea9a9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 193 lines to infinigen/assets/utils/uv.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit a31095485520b02dc5ece45ecd70c26a51b71c65
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 43 lines to infinigen/assets/utils/extract_nodegroup_parts.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit dd6f5c0abad11a70d927bdd1648248d34452721d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 53 lines to infinigen/assets/utils/autobevel.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 0fe354525ad8d697030e48a8d4b8170cc5978a7e
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 142 lines to infinigen/assets/utils/shapes.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit c19ae3b741e9abcd6f57c2c7b4a9a85ad0e7c978
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 87 lines to infinigen/assets/utils/bbox_from_mesh.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit fd4750ea9cdd640d1bead4123862fc58e702c813
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 3 lines to infinigen/assets/wall_decorations/balloon.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 70dec85102d7e06a7c184f874d65f4a5d1b9a1c3
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 75 lines to infinigen/assets/wall_decorations/balloon.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 827c956af9ffe26163a15f3ce028882451a1b63f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 1 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit f1196a2bbea49300cdf930048952fd2348510dd5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 18 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 8408175e69922f09dfae7303e3910cdd65cbb3fb
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 67 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 0842cb85156afa5cd2134b40789e999760b484eb
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 200 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 73e73f8501fb83b2124e07f31e21474d3032679c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 4 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit b774582124d00c9f5189389641081efe1e62010d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 13 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 0e3c27bcca6f5784c424b557c2c12f8f28eb6e43
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 73 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 9b5e875b969e9cd171c2bb134765b4ed03c7cebf
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 8 lines to infinigen/assets/wall_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit c8b575819e9c53cada27d62494de47834a00824d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 126 lines to infinigen/assets/wall_decorations/wall_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 339c93bafe702eaf0cf8178de5e134c2d6cc9f86
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 14 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit dcb850eb3e113c62b97dabdb10fb2c017c53cc4b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 20 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 610416aee0efe06f47b8b19a2354e24b64220d1a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 157 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 214f6613e5a7b14c9e10d678cf4915f9794c2463
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 2 lines to infinigen/assets/organizer/basket.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit b7222b97d93b94bd856259eea4cdfa7a071b205c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 304 lines to infinigen/assets/organizer/basket.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit b0a60402c9290eef410323fc7f5bffc7020520fb
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 9 lines to infinigen/assets/organizer/__init__.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit e86fd25ea089d46cbf1b9e718a458b55c64bb331
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 3 lines to infinigen/assets/organizer/hook.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 2a2cc9419a8e7644666ba2192b92bbae84ebbb4f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 384 lines to infinigen/assets/organizer/hook.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 8b1ad27d5042c2af6ac6150766bb4f6c19297ec1
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 2 lines to infinigen/assets/organizer/plate_rack.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit d21911b57df052d0635368e842fa1018838d656c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 333 lines to infinigen/assets/organizer/plate_rack.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit fc36d0de783f0fd1fedbf095f233f1af3d28f3f5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 1 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 2e5601f935772a8b6705c1acc0ade2fbb19a0ed4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 32 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit de924b79140f35fe02778cee2bc3e4de444e7e47
+Author: pvl-bot
+Date: Sun Jun 16 23:16:29 2024 -0700
+
+ Add 48 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 8ec2597f07d4ca21d58a8139fa50929f2017208a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 172 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Zeyu Ma.
+
+commit d9bee11e8a63829f6693af71c8e58120733807dc
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 336 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit b8f867ba511a7a7567c904e322ac732f68ecc807
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 626 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 2b64c425c2d3febcb39841bec831b101731ed4de
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 4 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit e67d3495bc2924f81722e6b0da320e5ae09c355b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 5 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 4799521049fb41d543dd5d1200b00c841d6d7f85
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 21 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 25992cd5d74711dd80d77009fb519c0d90a638b9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 191 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 0265f6788749baf227f69a7664ea8e64df10a864
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 1 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit bfa4527c8801880c0a5c30b07e9d53c10c0c7182
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 2 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 81758e1b71406d33a2f7fc22fd5ba3c4ff2fef55
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 2 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 85b8c3ff277b555afcefc7c5e966319ab77db9b0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 26 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Zeyu Ma.
+
+commit 379bf0e8ea6906515d9ff926f0ecc5acc01cd336
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 41 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit d681b3936397cc62f6492d82c099652536f49164
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 118 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit d774272384c322edbeea24f9114c5992f0cb37e7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 262 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit 374cbbaac1c4eea9c6d91e7ba2ce1d36319d4c36
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 37 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Zeyu Ma.
+
+commit fd831e3cbaccad056d94cbb3143ae7659471023a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 57 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit f04c5ed58c8c41f3b76df5376a34149d1b8cbeab
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 191 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit dfbf77ac8e0e5772687d11ea72705c5791ce8fb0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 644 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 07d44cc326a5973bdd5274807e18686205e994e4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 1 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 81af6f1ba6fab5799409f6d16490dc95488cc88f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 35 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Zeyu Ma.
+
+commit d1b0d664f5fc10fe7bd9a2bde80f39261e998085
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 42 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit cc2306691abc1134c6dc47fccf6d3d0f0c4fee2c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 223 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 9dfe85ec2044f8d6c7036b4e29ee61f4ab2d442b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 434 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Hongyu Wen.
+
+commit c9a029331d47dfab5af97347a031ade62b0896a5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 315fa23a929e3d19ecbf9d7ba7abe40aab81764f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit b7a4432f6f585f385a8b62aeb5827c03474663e4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 6 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit d92e73376f735bcf8d0c197783d94695d9198ab4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 6 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ce87ddafd34eabd3636d0a4fda3c51b0fa180332
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 724 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 34d20a32898db715b2010b0ab81d6d0055b9c488
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 3 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 7115cd353c1bbac5725b96a51dbcfc605fcfbdb2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 7 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit cc1a3aeca3404dcd57ae3001e7790dcfc12e8653
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 9 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 4f577be8db059becd357584229140c0044fe694b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 13 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 96e55c262af4b0b7aaae608f1b9d12109b7d549b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:28 2024 -0700
+
+ Add 16 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit 6774437197583f4ecd75f4295faf05c9fd487bd5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 280 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 6cdc8add656110b67780bacad7a80c59168a1f41
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 57f309a93ce2982385f274c02232f8da5537a1c5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 3 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit ab10ebe25105e3c4cb2b9021ae58f929c4e348e6
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 7 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit 807b5ae03ffee1915c9b70879e9ae22716eebfc7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 9 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 746130cc91509a702372fb73bafe87fffc82d89a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 215 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 34ce110ad247a979ff084aeb157632f7b918bfc2
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit dd191be0b76fcd7d89dc78c7303f2c4b8d5113d9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 5a83521268033ec429ffb602eacb6228ba614663
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 340 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 349792226fdbc18192ce20105d4157119b77272a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 526 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 5d8cfa5c8b79e23da3069153e05c039a0869df4d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit eb7a35d058bba82496ea4279711b47241ad3560b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 11 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 426d5616410ee2debb5f755ae3ea227e39b245b0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 43 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 927ccead91e369b8d8b898cec3335f0ed227db9a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 211 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit de585591a0a25d94f4fcd33305b3a88e0eb3b6a9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 154 lines to infinigen/assets/shelves/countertop.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit d380d10d68483cd2f6f18bbd73611d287571e8f9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 92786223272dca3624d78db73fc085e30338ce2d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 7 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit d4608181ffc6d32bcd000cc37b6716b94d5b5b19
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 52 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 7bd931a05212199cf33b3febbf4b0c44871f111d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 784999d4ce19cbe68f3219156bb5b5c06f2d4ef8
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 264 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 2d3cd29f4c5df2915a6e0d58357f0531a2bd0209
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 737 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit b8416ef5fd478a78c17d3f4ba5267b9db0d2da38
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit c37d776faa5b6c34a1a62aec0f103b8d30215341
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 32e51aa4e39729b894318f63fc78759754525116
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit e160187fe604413e3832a5b3a90612563a2786cc
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 5 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos.
+
+commit 21fe61b9328ede5c760bdce63e96bb9c5e96af4f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 6 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan.
+
+commit 13f9825b0a65293f5494151a2011856dcdbff28b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 68 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 9404ff0db2554c8e83f9f1531b912a9f6a1e7ef8
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 137 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Yiming Zuo.
+
+commit d33d7dc817b892a52c8aaa4aed6c890b003b7aae
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 3c99cc40045179181c93a675c84d9598ef74b704
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 089236016e23143ef0203da5084441a19a6879b7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 8 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 4fcf6d2b11ba4bf754d5bdcbf0f168457b50359f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 051d5676fcbc3d18491bae10975674913a7a7977
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 3827bdf2cb56f77cb9231d1a35e8e2225ef2b29d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:27 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit db936bbe8740e8639ed913de8791e568f7670ffb
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 612 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit b065b73beb885bda17926a45be88a1cfed855266
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit ba6dc0c4458fb68ee2c641bf774426165e08f25f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit e1fab8bbeaa6e15eaa83e32f2f0ef017db1ab6a9
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 159 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 0f92df4aaccf410323c45b22fd89c24a7d7019de
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 356 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit dfe9857289f6a1714c6291b9ab584e7785eef8a4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 25f3196595889e639620068731645520c741895d
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 1 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 37370c336faf3ab069927173000e7bb3925027a5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 4 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit baeebb9fe2c85dae745a3e44b16bfd7b712176c7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 410 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit 90269abe640d3956f2a01b8ab742b37042269655
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 2 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot.
+
+commit 08824a214e568656d16cddb704b9cf8dd1bb0845
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 3 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit af3b2672c245d8f8e364e765fe417a824c9e5f4a
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 9 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by David Yan.
+
+commit f04b47fb35ac82e1a2ab2bc94abe40843e777df1
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 36 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit afc2fbc10eb643bc29fc880ef56a98cffbc88ffd
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 850 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han.
+
+commit c82980a2394e23e9e49c93776f27e64967ee4a15
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 6 lines to infinigen/assets/bathroom/toilet.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 10beed29137fe07be80d492be288e6c79a632ab5
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 286 lines to infinigen/assets/bathroom/toilet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 25e5980e2c04f6281c86c3f4bf7026bc8dcfa711
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 11 lines to infinigen/assets/bathroom/hardware.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit a17feca0bbf006a8542ebf7b7d038e8f5d3659c7
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 110 lines to infinigen/assets/bathroom/hardware.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit ad6bfec63e6e5ee2b301230df1f5c9f11d5f761f
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 13 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit d49a0a4ff75e9d1bc668e355279867e9675b56c0
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 40 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick.
+
+commit 8013bd9f2114b3651c6510c5292a6b8eaaa6be9b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 236 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 279e32494c2478cb90217a94ea605890bb59ea98
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 8 lines to infinigen/assets/bathroom/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit debc696443bab1780457f9bddbe11281b8278002
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 5 lines to infinigen/assets/bathroom/bathroom_sink.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 227a01d1219eaf2c6fe3d874296c7bc855f79db3
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 138 lines to infinigen/assets/bathroom/bathroom_sink.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 49ddc761ef592c24afa073110929264624adb507
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 6 lines to infinigen/assets/clothes/shirt.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 268a5a1143fdfff604f9f4805fc3641697a22318
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 65 lines to infinigen/assets/clothes/shirt.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit f2cff82c97098b5b5ad97d9cdb719bb526ab0ee1
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 9 lines to infinigen/assets/clothes/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit c44d205f50af4d2a6b59e554930f1f9909206c8b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 6 lines to infinigen/assets/clothes/towel.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 0bd622a3b3d22f8ad306a46f95cf5eb516bad5a4
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 117 lines to infinigen/assets/clothes/towel.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 1da9534a168e4d9cbeb7b890eb1bf2dede4f134c
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 6 lines to infinigen/assets/clothes/blanket.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 4cbf5b737505da2094fa487bbb132395b06740cc
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 73 lines to infinigen/assets/clothes/blanket.py. Contributed as part of Infinigen-Indoors by Lingjie Mei.
+
+commit 358952c780386d3e781d8d8a181d984472f9258b
+Author: pvl-bot
+Date: Sun Jun 16 23:16:26 2024 -0700
+
+ Add 6 lines to infinigen/assets/clothes/pants.py. Contributed as part of Infinigen-Indoors by Meenal Parakh.
+
+commit 71be8d430897865bc395745e86fddee4844b6469
+Author: pvl-bot