From d603af095abcf0b7f69aa61b668bb758a7882851 Mon Sep 17 00:00:00 2001 From: pvl-bot Date: Sun, 16 Jun 2024 23:16:12 -0700 Subject: [PATCH 001/727] Add files that were renamed/deleted as part of Infinigen Indoors --- infinigen/OcMesher | 2 +- infinigen/assets/materials/wood.py | 89 ----------- infinigen/assets/utils/tag.py | 149 ------------------ .../configs/opengl_gt_noshortrender.gin | 7 - .../configs/disable_assets/no_assets.gin | 47 ------ .../configs/disable_assets/no_creatures.gin | 4 - .../configs/disable_assets/no_particles.gin | 5 - .../configs/disable_assets/no_rocks.gin | 2 - .../configs/performance/simple.gin | 5 - .../configs/scene_types/cave.gin | 60 ------- .../configs/scene_types/coral_reef.gin | 11 -- .../configs/scene_types/desert.gin | 46 ------ .../configs/scene_types/forest.gin | 58 ------- .../configs/scene_types/kelp_forest.gin | 24 --- .../configs/scene_types/plain.gin | 28 ---- .../configs/scene_types/under_water.gin | 81 ---------- infinigen_examples/configs/trailer_video.gin | 8 - .../base_surface_registry.gin | 0 .../{configs => configs_indoor}/natural.gin | 0 .../{configs => configs_nature}/.gitignore | 0 .../asset_demo.gin | 4 +- .../{configs => configs_nature}/base.gin | 128 +++++++-------- .../configs_nature/base_surface_registry.gin | 67 ++++++++ .../disable_assets/no_assets.gin | 47 ++++++ .../disable_assets/no_creatures.gin | 4 + .../disable_assets/no_particles.gin | 5 + .../disable_assets/no_rocks.gin | 2 + .../extras/experimental.gin | 4 +- .../extras/overhead.gin | 8 +- .../extras/stereo_training.gin | 6 +- .../extras/use_cached_fire.gin | 10 +- .../extras/use_on_the_fly_fire.gin | 8 +- .../{configs => configs_nature}/monocular.gin | 0 infinigen_examples/configs_nature/natural.gin | 3 + .../noisy_video.gin | 0 .../palette/desert.json | 0 .../configs_nature/palette/mountain soil.json | 47 ++++++ .../palette/sandstone.json | 0 .../palette/water.json | 0 .../performance/dev.gin | 10 +- .../performance/fast_terrain_assets.gin | 0 .../performance/high_quality_terrain.gin | 0 .../performance/reuse_terrain_assets.gin | 0 .../configs_nature/performance/simple.gin | 5 + .../scene_types/arctic.gin | 12 +- .../scene_types/canyon.gin | 8 +- .../configs_nature/scene_types/cave.gin | 65 ++++++++ .../scene_types/cliff.gin | 10 +- .../scene_types/coast.gin | 16 +- .../configs_nature/scene_types/coral_reef.gin | 11 ++ .../configs_nature/scene_types/desert.gin | 46 ++++++ .../configs_nature/scene_types/forest.gin | 58 +++++++ .../scene_types/kelp_forest.gin | 24 +++ .../scene_types/mountain.gin | 8 +- .../configs_nature/scene_types/plain.gin | 28 ++++ .../scene_types/river.gin | 18 ++- .../scene_types/snowy_mountain.gin | 10 +- .../scene_types/under_water.gin | 82 ++++++++++ .../scene_types_fluidsim/simulated_river.gin | 11 +- .../scene_types_fluidsim/tilted_river.gin | 17 +- .../configs_nature/trailer_video.gin | 8 + .../list_nature_materials.txt} | 0 .../list_nature_meshes.txt} | 0 tests/{ => core}/test_execute_tasks.py | 28 +--- tests/integration/conftest.py | 18 --- tests/test_materials_basic.py | 27 ---- tests/test_meshes_basic.py | 22 --- tests/utils.py | 50 ------ 68 files changed, 662 insertions(+), 899 deletions(-) delete mode 100644 infinigen/assets/materials/wood.py delete mode 100644 infinigen/assets/utils/tag.py delete mode 100644 infinigen/datagen/configs/opengl_gt_noshortrender.gin delete mode 100644 infinigen_examples/configs/disable_assets/no_assets.gin delete mode 100644 infinigen_examples/configs/disable_assets/no_creatures.gin delete mode 100644 infinigen_examples/configs/disable_assets/no_particles.gin delete mode 100644 infinigen_examples/configs/disable_assets/no_rocks.gin delete mode 100644 infinigen_examples/configs/performance/simple.gin delete mode 100644 infinigen_examples/configs/scene_types/cave.gin delete mode 100644 infinigen_examples/configs/scene_types/coral_reef.gin delete mode 100644 infinigen_examples/configs/scene_types/desert.gin delete mode 100644 infinigen_examples/configs/scene_types/forest.gin delete mode 100644 infinigen_examples/configs/scene_types/kelp_forest.gin delete mode 100644 infinigen_examples/configs/scene_types/plain.gin delete mode 100644 infinigen_examples/configs/scene_types/under_water.gin delete mode 100644 infinigen_examples/configs/trailer_video.gin rename infinigen_examples/{configs => configs_indoor}/base_surface_registry.gin (100%) rename infinigen_examples/{configs => configs_indoor}/natural.gin (100%) rename infinigen_examples/{configs => configs_nature}/.gitignore (100%) rename infinigen_examples/{configs => configs_nature}/asset_demo.gin (71%) rename infinigen_examples/{configs => configs_nature}/base.gin (67%) create mode 100644 infinigen_examples/configs_nature/base_surface_registry.gin create mode 100644 infinigen_examples/configs_nature/disable_assets/no_assets.gin create mode 100644 infinigen_examples/configs_nature/disable_assets/no_creatures.gin create mode 100644 infinigen_examples/configs_nature/disable_assets/no_particles.gin create mode 100644 infinigen_examples/configs_nature/disable_assets/no_rocks.gin rename infinigen_examples/{configs => configs_nature}/extras/experimental.gin (56%) rename infinigen_examples/{configs => configs_nature}/extras/overhead.gin (59%) rename infinigen_examples/{configs => configs_nature}/extras/stereo_training.gin (86%) rename infinigen_examples/{configs => configs_nature}/extras/use_cached_fire.gin (62%) rename infinigen_examples/{configs => configs_nature}/extras/use_on_the_fly_fire.gin (76%) rename infinigen_examples/{configs => configs_nature}/monocular.gin (100%) create mode 100644 infinigen_examples/configs_nature/natural.gin rename infinigen_examples/{configs => configs_nature}/noisy_video.gin (100%) rename infinigen_examples/{configs => configs_nature}/palette/desert.json (100%) create mode 100644 infinigen_examples/configs_nature/palette/mountain soil.json rename infinigen_examples/{configs => configs_nature}/palette/sandstone.json (100%) rename infinigen_examples/{configs => configs_nature}/palette/water.json (100%) rename infinigen_examples/{configs => configs_nature}/performance/dev.gin (61%) rename infinigen_examples/{configs => configs_nature}/performance/fast_terrain_assets.gin (100%) rename infinigen_examples/{configs => configs_nature}/performance/high_quality_terrain.gin (100%) rename infinigen_examples/{configs => configs_nature}/performance/reuse_terrain_assets.gin (100%) create mode 100644 infinigen_examples/configs_nature/performance/simple.gin rename infinigen_examples/{configs => configs_nature}/scene_types/arctic.gin (76%) rename infinigen_examples/{configs => configs_nature}/scene_types/canyon.gin (62%) create mode 100644 infinigen_examples/configs_nature/scene_types/cave.gin rename infinigen_examples/{configs => configs_nature}/scene_types/cliff.gin (67%) rename infinigen_examples/{configs => configs_nature}/scene_types/coast.gin (73%) create mode 100644 infinigen_examples/configs_nature/scene_types/coral_reef.gin create mode 100644 infinigen_examples/configs_nature/scene_types/desert.gin create mode 100644 infinigen_examples/configs_nature/scene_types/forest.gin create mode 100644 infinigen_examples/configs_nature/scene_types/kelp_forest.gin rename infinigen_examples/{configs => configs_nature}/scene_types/mountain.gin (55%) create mode 100644 infinigen_examples/configs_nature/scene_types/plain.gin rename infinigen_examples/{configs => configs_nature}/scene_types/river.gin (67%) rename infinigen_examples/{configs => configs_nature}/scene_types/snowy_mountain.gin (77%) create mode 100644 infinigen_examples/configs_nature/scene_types/under_water.gin rename infinigen_examples/{configs => configs_nature}/scene_types_fluidsim/simulated_river.gin (75%) rename infinigen_examples/{configs => configs_nature}/scene_types_fluidsim/tilted_river.gin (56%) create mode 100644 infinigen_examples/configs_nature/trailer_video.gin rename tests/{test_materials_basic.txt => assets/list_nature_materials.txt} (100%) rename tests/{test_meshes_basic.txt => assets/list_nature_meshes.txt} (100%) rename tests/{ => core}/test_execute_tasks.py (60%) delete mode 100644 tests/integration/conftest.py delete mode 100644 tests/test_materials_basic.py delete mode 100644 tests/test_meshes_basic.py delete mode 100644 tests/utils.py 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/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/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/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_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/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/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/.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_nature/palette/mountain soil.json b/infinigen_examples/configs_nature/palette/mountain soil.json new file mode 100644 index 000000000..b76a56958 --- /dev/null +++ b/infinigen_examples/configs_nature/palette/mountain soil.json @@ -0,0 +1,47 @@ +{ + "color": { + "0": "#916036", + "1": "#CFA677", + "3": "#384946", + "5": "#4C4623", + "6": "#9D884E", + "8": "#A29D81", + "9": "#51250A" + }, + "hsv": [ + [0.07657200610458859, 0.6218981568424602, 0.568095301296071], + [0.08871291011143334, 0.42452686112786103, 0.8104250687813854], + [0.6140700215233733, 0.13288612784624504, 0.8730243901503021], + [0.47346124527783656, 0.22768637564378527, 0.28529322850725286], + [0.5961218425011782, 0.49810503051093324, 0.7223871831892953], + [0.14544462943019612, 0.5389329914616029, 0.29703170640400595], + [0.12202185913813124, 0.49872400694381797, 0.614810311196337], + [0.02216908447486423, 0.027253574342521403, 0.9994307999673898], + [0.13984012270413265, 0.20105405545965413, 0.6344186455983115], + [0.0620713740714641, 0.8670860688594698, 0.3201029563817151] + ], + "std": [ + [0.01789703571127652,0.0,0.0,-0.12597331599571596,0.1291790795498408,0.0,0.014011416277454765,0.016482971792371452,0.13343880210454545], + [0.007744047862909046,0.0,0.0,0.012097193794030701,0.051526477352250744,0.0,0.003980522892370001,-0.019226818357123424,0.06316769321252796], + [0.09980781923323018,0.0,0.0,-0.02751107358853669,0.07348168453829067,0.0,0.03823990807304074,0.007156125966110767,0.09896511745818332], + [0.1554993390085664,0.0,0.0,0.06533844291739956,0.13682650189476622,0.0,0.009608147854344253,-0.058398154335554245,0.09181325340416716], + [0.010030408116143077,0.0,0.0,0.036937285719487165,0.14591077523998403,0.0,-0.02501126600237874,-0.026409038081482275,0.10355895739448526], + [0.06474239210522445,0.0,0.0,0.008133894434491999,0.20737552443259993,0.0,-0.007359426282875676,-0.05112646185112438,0.09877621827546076], + [0.05182286396833515,0.0,0.0,0.051138327526411384,0.14230590221035327,0.0,0.0427792445588568,0.0022117825436210794,0.13239327935553516], + [0.05822501097377041,0.0,0.0,0.07131802897006612,0.005299444518355579,0.0,-0.0012379613670810304,-5.4681679491867775e-05,0.0033877668057344145], + [0.07661598914233601,0.0,0.0,-0.0018042105407654976,0.12477201699512865,0.0,-0.038383553414123014,-0.13518589408121312,0.21018245422010579], + [0.03650937782912379,0.0,0.0,-0.024334393960077933,0.09154625092902843,0.0,0.0375423755906796,-0.0369424430932128,0.12079544868468167] + ], + "prob": [ + 0.18173535415922157, + 0.12693188009479223, + 0.11203245756478104, + 0.10729577747369581, + 0.10508095198299697, + 0.08720755326805475, + 0.08547331599457036, + 0.06928970489547422, + 0.06438450615250647, + 0.060568498413906664 + ] +} 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/tests/test_materials_basic.txt b/tests/assets/list_nature_materials.txt similarity index 100% rename from tests/test_materials_basic.txt rename to tests/assets/list_nature_materials.txt diff --git a/tests/test_meshes_basic.txt b/tests/assets/list_nature_meshes.txt similarity index 100% rename from tests/test_meshes_basic.txt rename to tests/assets/list_nature_meshes.txt diff --git a/tests/test_execute_tasks.py b/tests/core/test_execute_tasks.py similarity index 60% rename from tests/test_execute_tasks.py rename to tests/core/test_execute_tasks.py index 7b3878fd9..0ddb01e9c 100644 --- a/tests/test_execute_tasks.py +++ b/tests/core/test_execute_tasks.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 + from pathlib import Path from types import SimpleNamespace import logging @@ -12,28 +18,11 @@ from infinigen.core.placement import camera from infinigen.core import init -from utils import setup_gin - -''' -@pytest.mark.order('last') -def test_noassets_noterrain(): - args = SimpleNamespace( - input_folder=None, - output_folder='/tmp/test_noassets_noterrain', - seed="0", - task='coarse', - configs=['desert.gin', 'simple.gin', 'no_assets.gin'], - overrides=['compose_scene.generate_resolution = (480, 270)'], - task_uniqname='coarse', - loglevel=logging.DEBUG - ) - generate_nature.main(args) -''' +from infinigen_examples.util.test_utils import setup_gin -@pytest.mark.ci def test_compose_cube(): - setup_gin() + setup_gin('infinigen_examples/configs_nature') def compose_cube(output_folder, scene_seed, **params): camera_rigs = camera.spawn_camera_rigs() @@ -51,4 +40,3 @@ def compose_cube(output_folder, scene_seed, **params): frame_range=[0, 100], camera_id=(0, 0) ) - diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index 90d43591f..000000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -def pytest_addoption(parser): - parser.addoption( "--dir",action="store") - parser.addoption( "--num",action="store") - parser.addoption( "--days",action="store") - -@pytest.fixture() -def dir(request): - return request.config.getoption("--dir") - -@pytest.fixture() -def num(request): - return request.config.getoption("--num") - -@pytest.fixture() -def days(request): - return request.config.getoption("--days") \ No newline at end of file diff --git a/tests/test_materials_basic.py b/tests/test_materials_basic.py deleted file mode 100644 index c2d3aba9e..000000000 --- a/tests/test_materials_basic.py +++ /dev/null @@ -1,27 +0,0 @@ -from pathlib import Path -import importlib - -import pytest -import bpy -import gin - -from infinigen.core.util import blender as butil - -from utils import ( - setup_gin, - load_txt_list, - import_item -) - -setup_gin() - -@pytest.mark.ci -@pytest.mark.parametrize('pathspec', load_txt_list('test_materials_basic.txt')) -def test_material_runs(pathspec, **kwargs): - - butil.clear_scene() - bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=5) - asset = bpy.context.active_object - - mat = import_item(pathspec) - mat.apply(asset) \ No newline at end of file diff --git a/tests/test_meshes_basic.py b/tests/test_meshes_basic.py deleted file mode 100644 index e840a1446..000000000 --- a/tests/test_meshes_basic.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path - -import pytest -import bpy -import gin - -from infinigen.core.util import blender as butil - -from utils import ( - setup_gin, - import_item, - load_txt_list, - check_factory_runs -) - -setup_gin() - -@pytest.mark.ci -@pytest.mark.parametrize('pathspec', load_txt_list('test_meshes_basic.txt')) -def test_factory_runs(pathspec, **kwargs): - fac_class = import_item(pathspec) - check_factory_runs(fac_class, **kwargs) \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 593fce048..000000000 --- a/tests/utils.py +++ /dev/null @@ -1,50 +0,0 @@ - -from pathlib import Path -import importlib - -import gin -import bpy - -from infinigen.core import surface -from infinigen.core.util import blender as butil -from infinigen.core import init - -def setup_gin(configs=None, overrides=None): - - gin.clear_config() - init.apply_gin_configs( - configs_folder='infinigen_examples/configs', - configs=configs, - overrides=overrides, - skip_unknown=True - ) - surface.registry.initialize_from_gin() - - -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): - res = (Path(__file__).parent/path).read_text().splitlines() - res = [f.strip() for f in res if not f.startswith('#')] - res = [f for f in res if len(f) > 0] - return sorted(res) - -def check_factory_runs(fac_class, seed1=0, seed2=0, distance_m=50): - butil.clear_scene() - fac = fac_class(seed1) - asset = fac.spawn_asset(seed2, distance=distance_m) - - for o in butil.iter_object_tree(asset): - for i, slot in enumerate(o.material_slots): - if slot.material is None: - raise ValueError(f'{asset.name=} {o.name=} had material slot {i=} {slot=} with {slot.material=}') - - assert isinstance(asset, bpy.types.Object) \ No newline at end of file From fc46fe0a25ea01aa5f93a2477b1e6dd0dab1d890 Mon Sep 17 00:00:00 2001 From: pvl-bot Date: Sun, 16 Jun 2024 23:16:12 -0700 Subject: [PATCH 002/727] Changes to existing files contributed as part of Infinigen-Indoors. Contributed by all authors - we were unable to create correct per-author commits for edits to existing files. --- .github/workflows/checks.yml | 10 +- docs/CHANGELOG.md | 5 + docs/ConfiguringInfinigen.md | 36 +- docs/ExportingToExternalFileFormats.md | 9 +- docs/Installation.md | 23 +- infinigen/__init__.py | 2 +- infinigen/assets/cactus/columnar.py | 3 +- infinigen/assets/cactus/generate.py | 29 +- infinigen/assets/cactus/globular.py | 8 +- infinigen/assets/cactus/kalidium.py | 6 +- infinigen/assets/cactus/pricky_pear.py | 8 +- infinigen/assets/cactus/spike.py | 30 +- infinigen/assets/corals/diff_growth.py | 2 +- infinigen/assets/corals/elkhorn.py | 11 +- infinigen/assets/corals/fan.py | 7 +- infinigen/assets/corals/generate.py | 23 +- infinigen/assets/corals/laplacian.py | 2 +- infinigen/assets/corals/reaction_diffusion.py | 2 +- infinigen/assets/corals/star.py | 10 +- infinigen/assets/corals/tentacles.py | 16 +- infinigen/assets/corals/tree.py | 5 +- infinigen/assets/corals/tube.py | 2 +- infinigen/assets/creatures/beetle.py | 2 +- infinigen/assets/creatures/bird.py | 2 +- infinigen/assets/creatures/carnivore.py | 5 +- infinigen/assets/creatures/crustacean.py | 15 +- infinigen/assets/creatures/fish.py | 2 +- infinigen/assets/creatures/herbivore.py | 4 +- .../assets/creatures/insects/dragonfly.py | 18 +- infinigen/assets/creatures/jellyfish.py | 18 +- infinigen/assets/creatures/parts/beak.py | 2 +- infinigen/assets/creatures/parts/body.py | 2 +- .../creatures/parts/crustacean/antenna.py | 5 +- .../assets/creatures/parts/crustacean/body.py | 14 +- .../assets/creatures/parts/crustacean/claw.py | 5 +- .../assets/creatures/parts/crustacean/eye.py | 3 +- .../assets/creatures/parts/crustacean/leg.py | 5 +- .../assets/creatures/parts/crustacean/tail.py | 5 +- infinigen/assets/creatures/parts/eye.py | 2 +- infinigen/assets/creatures/parts/fin_old.py | 2 +- infinigen/assets/creatures/parts/foot.py | 2 +- .../assets/creatures/parts/generic_nurbs.py | 2 +- infinigen/assets/creatures/parts/head.py | 2 +- .../assets/creatures/parts/head_detail.py | 2 +- infinigen/assets/creatures/parts/hoof.py | 2 +- infinigen/assets/creatures/parts/horn.py | 2 +- infinigen/assets/creatures/parts/leg.py | 2 +- .../assets/creatures/parts/ridged_fin.py | 2 +- infinigen/assets/creatures/parts/tail.py | 4 +- infinigen/assets/creatures/parts/wings.py | 2 +- infinigen/assets/creatures/util/cloth_sim.py | 41 +- .../assets/creatures/util/geometry/curve.py | 12 +- .../assets/creatures/util/geonode_part.py | 44 +- infinigen/assets/creatures/util/part_util.py | 34 +- infinigen/assets/debris/lichen.py | 13 +- infinigen/assets/debris/moss.py | 27 +- infinigen/assets/debris/pine_needle.py | 2 +- infinigen/assets/deformed_trees/base.py | 5 +- infinigen/assets/deformed_trees/fallen.py | 9 +- infinigen/assets/deformed_trees/generate.py | 2 +- infinigen/assets/deformed_trees/hollow.py | 7 +- infinigen/assets/deformed_trees/rotten.py | 10 +- infinigen/assets/deformed_trees/truncated.py | 2 +- infinigen/assets/fluid/__init__.py | 6 + .../assets/fluid/cached_factory_wrappers.py | 4 + .../assets/fluid/fluid_scenecomp_additions.py | 6 + infinigen/assets/fluid/run_asset_cache.py | 7 +- infinigen/assets/fruits/general_fruit.py | 2 +- infinigen/assets/grassland/dandelion.py | 2 +- infinigen/assets/grassland/flower.py | 2 +- infinigen/assets/grassland/flowerplant.py | 2 +- infinigen/assets/grassland/grass_tuft.py | 2 +- infinigen/assets/leaves/leaf.py | 2 +- infinigen/assets/leaves/leaf_broadleaf.py | 2 +- infinigen/assets/leaves/leaf_ginko.py | 2 +- infinigen/assets/leaves/leaf_maple.py | 2 +- infinigen/assets/leaves/leaf_pine.py | 2 +- infinigen/assets/leaves/leaf_v2.py | 2 +- infinigen/assets/lighting/__init__.py | 11 +- infinigen/assets/lighting/caustics_lamp.py | 2 +- infinigen/assets/lighting/sky_lighting.py | 1 + infinigen/assets/materials/__init__.py | 53 ++ infinigen/assets/materials/chunkyrock.py | 34 +- infinigen/assets/materials/cobble_stone.py | 6 +- infinigen/assets/materials/dirt.py | 37 +- infinigen/assets/materials/sandstone.py | 8 +- infinigen/assets/materials/soil.py | 10 +- infinigen/assets/materials/stone.py | 28 +- .../assets/materials/utils/surface_utils.py | 52 +- infinigen/assets/materials/water.py | 15 +- infinigen/assets/mollusk/generate.py | 8 +- infinigen/assets/mollusk/shell.py | 8 +- infinigen/assets/mollusk/snail.py | 4 +- infinigen/assets/monocot/agave.py | 9 +- infinigen/assets/monocot/banana.py | 10 +- infinigen/assets/monocot/generate.py | 6 +- infinigen/assets/monocot/grasses.py | 12 +- infinigen/assets/monocot/growth.py | 23 +- infinigen/assets/monocot/kelp.py | 6 +- infinigen/assets/monocot/pinecone.py | 10 +- infinigen/assets/monocot/tussock.py | 4 +- infinigen/assets/monocot/veratrum.py | 26 +- infinigen/assets/mushroom/cap.py | 38 +- infinigen/assets/mushroom/generate.py | 6 +- infinigen/assets/mushroom/growth.py | 10 +- infinigen/assets/mushroom/stem.py | 8 +- infinigen/assets/rocks/blender_rock.py | 6 +- infinigen/assets/rocks/boulder.py | 7 +- infinigen/assets/rocks/glowing_rocks.py | 11 +- infinigen/assets/rocks/pile.py | 6 +- infinigen/assets/scatters/ground_twigs.py | 6 +- infinigen/assets/scatters/ivy.py | 6 +- infinigen/assets/scatters/lichen.py | 12 +- infinigen/assets/scatters/mollusk.py | 5 +- infinigen/assets/scatters/moss.py | 8 +- infinigen/assets/scatters/mushroom.py | 2 +- infinigen/assets/scatters/slime_mold.py | 10 +- infinigen/assets/scatters/snow_layer.py | 2 +- infinigen/assets/scatters/utils/wind.py | 3 +- infinigen/assets/small_plants/fern.py | 2 +- infinigen/assets/small_plants/leaf_general.py | 2 +- infinigen/assets/small_plants/leaf_heart.py | 2 +- .../assets/small_plants/num_leaf_grass.py | 2 +- infinigen/assets/small_plants/snake_plant.py | 2 +- infinigen/assets/small_plants/spider_plant.py | 2 +- infinigen/assets/small_plants/succulent.py | 2 +- infinigen/assets/trees/branch.py | 2 +- infinigen/assets/trees/generate.py | 11 +- infinigen/assets/trees/tree.py | 2 +- infinigen/assets/trees/tree_flower.py | 2 +- infinigen/assets/trees/utils/geometrynodes.py | 2 - infinigen/assets/trees/utils/materials.py | 5 +- .../assets/tropic_plants/leaf_banana_tree.py | 2 +- .../assets/tropic_plants/leaf_palm_tree.py | 2 +- infinigen/assets/underwater/seaweed.py | 12 +- infinigen/assets/underwater/urchin.py | 16 +- infinigen/assets/utils/decorate.py | 405 +++++++++----- infinigen/assets/utils/draw.py | 78 ++- infinigen/assets/utils/laplacian.py | 2 +- infinigen/assets/utils/mesh.py | 274 +++++++++- infinigen/assets/utils/misc.py | 90 +++- infinigen/assets/utils/nodegroup.py | 24 +- infinigen/assets/utils/object.py | 122 ++++- infinigen/assets/utils/reaction_diffusion.py | 2 +- infinigen/assets/weather/cloud/generate.py | 7 +- infinigen/assets/weather/particles.py | 2 +- infinigen/core/execute_tasks.py | 58 +- infinigen/core/init.py | 12 +- infinigen/core/nodes/compatibility.py | 6 + infinigen/core/nodes/node_info.py | 52 +- infinigen/core/nodes/node_utils.py | 24 +- infinigen/core/nodes/node_wrangler.py | 65 ++- infinigen/core/placement/__init__.py | 1 + infinigen/core/placement/animation_policy.py | 272 +++++++--- infinigen/core/placement/camera.py | 328 ++++++++---- infinigen/core/placement/density.py | 48 +- infinigen/core/placement/detail.py | 11 +- infinigen/core/placement/factory.py | 50 +- infinigen/core/placement/instance_scatter.py | 11 +- infinigen/core/placement/placement.py | 11 +- infinigen/core/rendering/post_render.py | 7 +- infinigen/core/rendering/render.py | 27 +- infinigen/core/rendering/resample.py | 1 - infinigen/core/surface.py | 116 ++-- infinigen/core/util/blender.py | 200 +++++-- infinigen/core/util/camera.py | 36 +- infinigen/core/util/color.py | 160 +++--- infinigen/core/util/logging.py | 12 +- infinigen/core/util/organization.py | 1 + infinigen/core/util/pipeline.py | 6 +- infinigen/core/util/random.py | 15 +- infinigen/datagen/configs/base.gin | 2 +- .../configs/compute_platform/local_128GB.gin | 2 +- .../configs/compute_platform/local_16GB.gin | 2 +- .../configs/compute_platform/local_256GB.gin | 5 + .../configs/compute_platform/local_64GB.gin | 2 +- .../configs/compute_platform/slurm.gin | 15 +- .../configs/compute_platform/slurm_1h.gin | 7 +- .../compute_platform/slurm_cpuheavy.gin | 2 +- .../compute_platform/slurm_high_memory.gin | 6 +- .../configs/data_schema/monocular_flow.gin | 2 +- .../configs/data_schema/stereo_video.gin | 2 +- .../gt_options/opengl_gt_noshortrender.gin | 2 +- infinigen/datagen/configs/upload.gin | 1 + infinigen/datagen/job_funcs.py | 52 +- infinigen/datagen/manage_jobs.py | 27 +- infinigen/datagen/util/cleanup.py | 1 - infinigen/datagen/util/upload_util.py | 4 +- infinigen/launch_blender.py | 6 + infinigen/terrain/__init__.py | 2 +- infinigen/terrain/core.py | 135 ++--- infinigen/terrain/elements/core.py | 10 + infinigen/terrain/utils/camera.py | 2 +- infinigen/terrain/utils/kernelizer_util.py | 2 +- infinigen/terrain/utils/mesh.py | 14 + .../tools/blendscript_import_infinigen.py | 7 +- infinigen/tools/blendscript_path_append.py | 6 + infinigen/tools/export.py | 502 ++++++++++++++---- infinigen/tools/suffixes.py | 3 +- infinigen/tools/terrain/palette/readme.md | 2 +- .../configs/palette/mountain soil.json | 47 -- infinigen_examples/generate_asset_demo.py | 10 +- .../generate_individual_assets.py | 296 ++++++----- infinigen_examples/generate_nature.py | 35 +- pyproject.toml | 67 ++- scripts/install/compile_terrain.sh | 2 +- setup.py | 9 + tests/integration/manual_integration_check.py | 14 +- tests/test_terrain_basic.py | 13 +- 209 files changed, 3392 insertions(+), 1696 deletions(-) delete mode 100644 infinigen_examples/configs/palette/mountain soil.json 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/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/Installation.md b/docs/Installation.md index dae6a8a57..c713017cc 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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: ![](demo4.png) -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/configs/palette/mountain soil.json b/infinigen_examples/configs/palette/mountain soil.json deleted file mode 100644 index b76a56958..000000000 --- a/infinigen_examples/configs/palette/mountain soil.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "color": { - "0": "#916036", - "1": "#CFA677", - "3": "#384946", - "5": "#4C4623", - "6": "#9D884E", - "8": "#A29D81", - "9": "#51250A" - }, - "hsv": [ - [0.07657200610458859, 0.6218981568424602, 0.568095301296071], - [0.08871291011143334, 0.42452686112786103, 0.8104250687813854], - [0.6140700215233733, 0.13288612784624504, 0.8730243901503021], - [0.47346124527783656, 0.22768637564378527, 0.28529322850725286], - [0.5961218425011782, 0.49810503051093324, 0.7223871831892953], - [0.14544462943019612, 0.5389329914616029, 0.29703170640400595], - [0.12202185913813124, 0.49872400694381797, 0.614810311196337], - [0.02216908447486423, 0.027253574342521403, 0.9994307999673898], - [0.13984012270413265, 0.20105405545965413, 0.6344186455983115], - [0.0620713740714641, 0.8670860688594698, 0.3201029563817151] - ], - "std": [ - [0.01789703571127652,0.0,0.0,-0.12597331599571596,0.1291790795498408,0.0,0.014011416277454765,0.016482971792371452,0.13343880210454545], - [0.007744047862909046,0.0,0.0,0.012097193794030701,0.051526477352250744,0.0,0.003980522892370001,-0.019226818357123424,0.06316769321252796], - [0.09980781923323018,0.0,0.0,-0.02751107358853669,0.07348168453829067,0.0,0.03823990807304074,0.007156125966110767,0.09896511745818332], - [0.1554993390085664,0.0,0.0,0.06533844291739956,0.13682650189476622,0.0,0.009608147854344253,-0.058398154335554245,0.09181325340416716], - [0.010030408116143077,0.0,0.0,0.036937285719487165,0.14591077523998403,0.0,-0.02501126600237874,-0.026409038081482275,0.10355895739448526], - [0.06474239210522445,0.0,0.0,0.008133894434491999,0.20737552443259993,0.0,-0.007359426282875676,-0.05112646185112438,0.09877621827546076], - [0.05182286396833515,0.0,0.0,0.051138327526411384,0.14230590221035327,0.0,0.0427792445588568,0.0022117825436210794,0.13239327935553516], - [0.05822501097377041,0.0,0.0,0.07131802897006612,0.005299444518355579,0.0,-0.0012379613670810304,-5.4681679491867775e-05,0.0033877668057344145], - [0.07661598914233601,0.0,0.0,-0.0018042105407654976,0.12477201699512865,0.0,-0.038383553414123014,-0.13518589408121312,0.21018245422010579], - [0.03650937782912379,0.0,0.0,-0.024334393960077933,0.09154625092902843,0.0,0.0375423755906796,-0.0369424430932128,0.12079544868468167] - ], - "prob": [ - 0.18173535415922157, - 0.12693188009479223, - 0.11203245756478104, - 0.10729577747369581, - 0.10508095198299697, - 0.08720755326805475, - 0.08547331599457036, - 0.06928970489547422, - 0.06438450615250647, - 0.060568498413906664 - ] -} 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_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_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/pyproject.toml b/pyproject.toml index fce75582e..7e8caa6fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,12 +10,12 @@ dynamic = ["version"] description = "Infinite Photorealistic Worlds using Procedural Generation" keywords = [ - "computer vision", - "data generation", + "computer vision", + "data generation", "procedural" ] classifiers = [ - "Framework :: Blender", + "Framework :: Blender", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10" ] @@ -29,22 +29,29 @@ dependencies = [ "geomdl", "gin_config>=0.5.0", "imageio", + "ipython", + "json5", + "landlab>=2.6.0", "matplotlib", + "networkx", "numpy", "opencv-python", + "pandas", "psutil", + "pycparser==2.22", + "pyrender", + "python-fcl", + "Rtree", "scikit-image", "scikit-learn", "scipy", + "shapely", "submitit", "tqdm", "trimesh", - "pandas", - "landlab>=2.6.0", - "pycparser", - "pyrender", - "pandas", "vnoise", + "zarr", + "networkx", ] [project.optional-dependencies] @@ -54,16 +61,25 @@ dev = [ "pytest-cov", "pytest-xdist", "pytest-timeout", - "ruff", + "pytype", + "ruff", + "isort", + "tabulate", # for integration test results +] + +vis = [ + "numba", # for ground truth visuals + "pyglet<2" # for trimesh_scene.show() ] + wandb = [ "wandb" ] [tool.setuptools] -# include-package-data is terribly named. package-data is still included if false, +# include-package-data is terribly named. package-data is still included if false, # just not the package-data setuptools would otherwise autogenerate from MANIFEST.in or version control -include-package-data = false +include-package-data = false [tool.setuptools.packages.find] include = ["infinigen*"] @@ -78,7 +94,7 @@ exclude = [ "*" = ["*.gin", "*.txt", "*.json"] -# Must be specified as paths relative to infinigen/ +# Must be specified as paths relative to infinigen/ "infinigen" = [ "terrain/**/*.soil", # extra files for SoilMachine "terrain/lib/**/*.so", # created by terrain compilation @@ -93,14 +109,26 @@ version = {attr = "infinigen.__version__"} [tool.pytest.ini_options] testpaths = "tests" junit_family = "xunit2" +markers = ["nature", "indoors", "skip_for_ci"] timeout = 240 +filterwarnings = [ + + "ignore:The value of the smallest subnormal for Date: Sun, 16 Jun 2024 23:16:12 -0700 Subject: [PATCH 003/727] Improvements to exporting.py, contributed by David Yan as part of Infinigen Indoors --- infinigen/core/util/exporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 7c7b9cbe7a1c1ace6bc9d12ddf63e27f36dec552 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 004/727] Add 328 lines to infinigen_examples/generate_asset_parameters.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../generate_asset_parameters.py | 328 ++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 infinigen_examples/generate_asset_parameters.py diff --git a/infinigen_examples/generate_asset_parameters.py b/infinigen_examples/generate_asset_parameters.py new file mode 100644 index 000000000..5af92f6a8 --- /dev/null +++ b/infinigen_examples/generate_asset_parameters.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: +# - 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 + +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) From 76a6b45c44f9449fde7d51e0e88c33aa6af12877 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 005/727] Add 1 lines to infinigen_examples/generate_asset_parameters.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/generate_asset_parameters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/generate_asset_parameters.py b/infinigen_examples/generate_asset_parameters.py index 5af92f6a8..eb40bd499 100644 --- a/infinigen_examples/generate_asset_parameters.py +++ b/infinigen_examples/generate_asset_parameters.py @@ -26,6 +26,7 @@ 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', From 68b87646d4d232e0303d8d63c0746a0e19b332ed Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 006/727] Add 356 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/generate_indoors.py | 356 +++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 infinigen_examples/generate_indoors.py diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py new file mode 100644 index 000000000..e4ab74e47 --- /dev/null +++ b/infinigen_examples/generate_indoors.py @@ -0,0 +1,356 @@ +# Copyright (c) Princeton University. +# 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 +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.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.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 + 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_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 + + logger.debug(overrides) + + terrain = Terrain( + scene_seed, + surface.registry, + task='coarse', + on_the_fly_asset_folder=output_folder / "assets" + ) + 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, + 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)) + + rooms_meshed = butil.get_collection('placeholders:room_meshes') + + #state.print() + state.to_json(output_folder / 'solve_state.json') + + 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 + ) + + 'nature_backdrop', + create_outdoor_backdrop, + terrain, + house_bbox=house_bbox, + cam=cam, + p=p, + params=overrides, + use_chance=False, + ) + 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 + + "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() + + + + 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'], + 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) + From d9cd7ad8a9f779d54445d19d975966d1513a490b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 007/727] Add 38 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/generate_indoors.py | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index e4ab74e47..98fc74990 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -6,8 +6,10 @@ 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', @@ -21,6 +23,7 @@ import trimesh from numpy import deg2rad +from infinigen.assets import (lighting) from infinigen.assets.utils.decorate import read_co from infinigen.terrain import Terrain from infinigen.assets.materials import invisible_to_camera @@ -49,6 +52,7 @@ 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, @@ -118,6 +122,7 @@ def default_greedy_stages(): 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 @@ -133,6 +138,8 @@ def default_greedy_stages(): task='coarse', on_the_fly_asset_folder=output_folder / "assets" ) + # placement.density.set_tag_dict(terrain.tag_dict) + p.run_stage('sky_lighting', lighting.sky_lighting.add_lighting, use_chance=False) consgraph = home_constraints() @@ -265,9 +272,12 @@ def solve_small(): 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') + p.run_stage('room_pillars', room_dec.room_pillars, state, rooms_split['wall'].objects, use_chance=False) + #state.print() state.to_json(output_folder / 'solve_state.json') @@ -316,8 +326,33 @@ def invisible_room_ceilings(): 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 + 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(): + if area.type == 'VIEW_3D': + area.spaces.active.region_3d.view_perspective = 'CAMERA' + break + break "whole_bbox": house_bbox, + def main(args): scene_seed = init.apply_scene_seed(args.seed) init.apply_gin_configs( @@ -327,8 +362,11 @@ def main(args): ) constants.initialize_constants() + 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) From 4c93f39b04580438820f77c92192245eaf380a13 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 008/727] Add 14 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen_examples/generate_indoors.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 98fc74990..8cc74c924 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -27,6 +27,7 @@ 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, @@ -129,16 +130,20 @@ def default_greedy_stages(): all_vars = [cu.variable_room, cu.variable_obj] @gin.configurable + 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 p.run_stage('sky_lighting', lighting.sky_lighting.add_lighting, use_chance=False) @@ -281,6 +286,8 @@ def solve_small(): #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: @@ -312,6 +319,7 @@ def invisible_room_ceilings(): use_chance=False ) + height = p.run_stage( 'nature_backdrop', create_outdoor_backdrop, terrain, @@ -320,6 +328,8 @@ def invisible_room_ceilings(): p=p, params=overrides, use_chance=False, + prereq='terrain', + default=0, ) if overrides.get('topview', False): rooms_split['exterior'].hide_viewport = True @@ -351,7 +361,11 @@ def invisible_room_ceilings(): break break + return { + "height_offset": height, "whole_bbox": house_bbox, + } + def main(args): scene_seed = init.apply_scene_seed(args.seed) From 6e56c2ef8cf444dec5db6fc42eba7a21d0b97186 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 009/727] Add 12 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/generate_indoors.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 8cc74c924..9c4037649 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -94,6 +94,7 @@ def default_greedy_stages(): 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)) @@ -130,6 +131,7 @@ def default_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) @@ -145,6 +147,7 @@ def add_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() @@ -241,6 +244,7 @@ def animate_cameras(): 'populate_intermediate_pholders', populate.populate_state_placeholders, solver.state, + filter=t.Semantics.AssetPlaceholderForChildren, final=False, use_chance=False ) @@ -280,8 +284,12 @@ def solve_small(): 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') @@ -331,6 +339,7 @@ def invisible_room_ceilings(): prereq='terrain', default=0, ) + if overrides.get('topview', False): rooms_split['exterior'].hide_viewport = True rooms_split['ceiling'].hide_viewport = True @@ -346,6 +355,7 @@ def invisible_room_ceilings(): 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 @@ -356,6 +366,7 @@ def invisible_room_ceilings(): 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 @@ -376,6 +387,7 @@ def main(args): ) 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) From ac4ee588f10b2fd8343a230279a376c7bde22d93 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 010/727] Add 3 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen_examples/generate_indoors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 9c4037649..54e28ecae 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -71,6 +71,8 @@ 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, @@ -376,6 +378,7 @@ def invisible_room_ceilings(): "height_offset": height, "whole_bbox": house_bbox, } + def main(args): From beaa75a14b16086a68b432e1b2e2e4cc54d55f04 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 011/727] Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen_examples/generate_indoors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 54e28ecae..63f76b799 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -421,3 +421,4 @@ def main(args): 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 From 10876a5221ccdf9f4cf84e08b19452ebecc5d794 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 012/727] Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen_examples/generate_indoors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 63f76b799..9cd459597 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -24,6 +24,7 @@ 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 ba1a909fd023965a10f9e875bc20e666131fb158 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 013/727] Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen_examples/generate_indoors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/generate_indoors.py b/infinigen_examples/generate_indoors.py index 9cd459597..80fac6cd8 100644 --- a/infinigen_examples/generate_indoors.py +++ b/infinigen_examples/generate_indoors.py @@ -402,6 +402,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', '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)') From 8c35601b5f56f1d78f094e10aed827c56073b602 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 014/727] Add 45 lines to infinigen_examples/asset_parameters.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/asset_parameters.py | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 infinigen_examples/asset_parameters.py 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, + }, + +} From 2ca810acd81851ecad18cefa09b4a109543071fb Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 015/727] Add 332 lines to infinigen_examples/indoor_asset_semantics.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/indoor_asset_semantics.py | 332 +++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 infinigen_examples/indoor_asset_semantics.py diff --git a/infinigen_examples/indoor_asset_semantics.py b/infinigen_examples/indoor_asset_semantics.py new file mode 100644 index 000000000..873eca13e --- /dev/null +++ b/infinigen_examples/indoor_asset_semantics.py @@ -0,0 +1,332 @@ +# Copyright (c) Princeton University. +# This 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, + } + + used_as[Semantics.AssetAsPlaceholder] = set() + + used_as[Semantics.AssetPlaceholderForChildren] = { + shelves.SimpleBookcaseFactory, + shelves.CellShelfFactory, + shelves.SingleCabinetFactory, + shelves.KitchenCabinetFactory, + shelves.LargeShelfFactory, + } + + 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, + } + + # endregion + + return used_as \ No newline at end of file From dc5bca37d1342c0ce603b038836f7dd56f89ac94 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:09 -0700 Subject: [PATCH 016/727] Add 7 lines to infinigen_examples/indoor_asset_semantics.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/indoor_asset_semantics.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen_examples/indoor_asset_semantics.py b/infinigen_examples/indoor_asset_semantics.py index 873eca13e..f0abf060a 100644 --- a/infinigen_examples/indoor_asset_semantics.py +++ b/infinigen_examples/indoor_asset_semantics.py @@ -327,6 +327,13 @@ def home_asset_usage(): 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 From 87ebffffef202bf884a85899ac737d5ddea4e7f4 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 017/727] Add 6 lines to infinigen_examples/indoor_asset_semantics.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen_examples/indoor_asset_semantics.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen_examples/indoor_asset_semantics.py b/infinigen_examples/indoor_asset_semantics.py index f0abf060a..9ae3196c2 100644 --- a/infinigen_examples/indoor_asset_semantics.py +++ b/infinigen_examples/indoor_asset_semantics.py @@ -284,6 +284,10 @@ def home_asset_usage(): wall_decorations.BalloonFactory, wall_decorations.MirrorFactory, wall_decorations.WallArtFactory, + shelves.SingleCabinetFactory, + shelves.KitchenCabinetFactory, + shelves.CellShelfFactory, + elements.NatureShelfTrinketsFactory } used_as[Semantics.AssetAsPlaceholder] = set() @@ -294,6 +298,8 @@ def home_asset_usage(): shelves.SingleCabinetFactory, shelves.KitchenCabinetFactory, shelves.LargeShelfFactory, + table_decorations.SinkFactory, + tables.TableCocktailFactory } used_as[Semantics.PlaceholderBBox] = { From 671b9655400a703201c384943c922f1c44055c22 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 018/727] Add 203 lines to infinigen_examples/generate_material_balls.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/generate_material_balls.py | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 infinigen_examples/generate_material_balls.py 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) From b82aa6f30dd949b2dbc7cbff90d0be588b76c0cd Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 019/727] Add 773 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../indoor_constraint_examples.py | 773 ++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100644 infinigen_examples/indoor_constraint_examples.py diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py new file mode 100644 index 000000000..46dcada2f --- /dev/null +++ b/infinigen_examples/indoor_constraint_examples.py @@ -0,0 +1,773 @@ +# Copyright (c) Princeton University. +# This 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 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}] + + doors = cutters[Semantics.Door] + + + #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) + + + + 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) + )) + + + 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: ( + )) + )) + + 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: ( + )) + )) + + 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) + ) + )) + + # 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 From ead7bde295b2aad2b532bae3fcfbed0fe6090843 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 020/727] Add 18 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- .../indoor_constraint_examples.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py index 46dcada2f..996d729f3 100644 --- a/infinigen_examples/indoor_constraint_examples.py +++ b/infinigen_examples/indoor_constraint_examples.py @@ -4,7 +4,10 @@ # Authors: Alexander Raistrick +from collections import OrderedDict + import numpy as np +import random from numpy.random import uniform, normal, randint import infinigen @@ -77,6 +80,8 @@ def home_constraints(): doors = cutters[Semantics.Door] + constraints = OrderedDict() + score_terms = OrderedDict() #region overall fullness @@ -100,6 +105,7 @@ def home_constraints(): score_terms['furniture_aesthetics'] = wallfurn.sum(lambda t: ( t.distance(wallfurn).hinge(0.2, 0.6).maximize(weight=0.6) + + )) constraints['storage'] = rooms.all(lambda r: ( @@ -162,6 +168,7 @@ def home_constraints(): + 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: ( @@ -181,6 +188,7 @@ def home_constraints(): 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) @@ -190,7 +198,9 @@ def home_constraints(): .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) @@ -212,6 +222,7 @@ def home_constraints(): (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: ( @@ -321,11 +332,13 @@ def home_constraints(): 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 @@ -340,6 +353,7 @@ def home_constraints(): 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: ( @@ -382,6 +396,7 @@ def home_constraints(): 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) @@ -415,6 +430,7 @@ def home_constraints(): )) )) + )) kitchen_appliances = obj[Semantics.KitchenAppliance] kitchen_appliances_big = kitchen_appliances.related_to(kitchens, cu.on_floor).related_to(kitchens, cu.against_wall) @@ -613,6 +629,7 @@ def home_constraints(): obj[Semantics.TableDisplayItem].related_to(t, cu.ontop).count().in_range(0, 1) * (obj[Semantics.OfficeShelfItem].related_to(t, cu.on).count() >= 0) )) + )) #endregion @@ -703,6 +720,7 @@ def home_constraints(): .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) From d1c6afc81a046c201c87dd02c8fe0c047c46fb88 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 021/727] Add 12 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/indoor_constraint_examples.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py index 996d729f3..b77d83a00 100644 --- a/infinigen_examples/indoor_constraint_examples.py +++ b/infinigen_examples/indoor_constraint_examples.py @@ -78,6 +78,8 @@ def home_constraints(): 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() @@ -105,6 +107,8 @@ def home_constraints(): 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) )) @@ -449,6 +453,10 @@ def home_constraints(): 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) )) )) @@ -460,6 +468,7 @@ def home_constraints(): * (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) )) )) @@ -469,6 +478,9 @@ def home_constraints(): .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 From 190701fabce3797f65a63c3ade589b06c582c9b2 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 022/727] Add 2 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen_examples/indoor_constraint_examples.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py index b77d83a00..d150acddf 100644 --- a/infinigen_examples/indoor_constraint_examples.py +++ b/infinigen_examples/indoor_constraint_examples.py @@ -733,11 +733,13 @@ def home_constraints(): )) )) + 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']: From 0339debc670a9078b6d7a9c39586befa2ce8d150 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 023/727] Add 1 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/indoor_constraint_examples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py index d150acddf..852c00e19 100644 --- a/infinigen_examples/indoor_constraint_examples.py +++ b/infinigen_examples/indoor_constraint_examples.py @@ -803,3 +803,4 @@ def home_constraints(): all_constraint_funcs = [ home_constraints +] From 21df81ea9cf88054d8a6678306497b0d97d59b73 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 024/727] Add 1 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen_examples/indoor_constraint_examples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/indoor_constraint_examples.py b/infinigen_examples/indoor_constraint_examples.py index 852c00e19..cda7d78b3 100644 --- a/infinigen_examples/indoor_constraint_examples.py +++ b/infinigen_examples/indoor_constraint_examples.py @@ -150,6 +150,7 @@ def home_constraints(): 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) From 4a50ac83d58018e461ed87842122d710292c12c2 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 025/727] Add 57 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/util/test_utils.py | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 infinigen_examples/util/test_utils.py diff --git a/infinigen_examples/util/test_utils.py b/infinigen_examples/util/test_utils.py new file mode 100644 index 000000000..3425bd511 --- /dev/null +++ b/infinigen_examples/util/test_utils.py @@ -0,0 +1,57 @@ +# Copyright (c) Princeton University. +# This 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), + 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 From 2146fd4d32a4f6387d3d719d3c71f9cbbac97efb Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 026/727] Add 2 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen_examples/util/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen_examples/util/test_utils.py b/infinigen_examples/util/test_utils.py index 3425bd511..761953c00 100644 --- a/infinigen_examples/util/test_utils.py +++ b/infinigen_examples/util/test_utils.py @@ -20,6 +20,8 @@ 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 ) From 6ef860069594558337ff61426755a5d0a67d9d28 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 027/727] Add 1 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/util/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/util/test_utils.py b/infinigen_examples/util/test_utils.py index 761953c00..2892bd7f4 100644 --- a/infinigen_examples/util/test_utils.py +++ b/infinigen_examples/util/test_utils.py @@ -35,6 +35,7 @@ def setup_gin(configs_folder, configs=None, overrides=None): def import_item(name): *path_parts, name = name.split('.') with gin.unlock_config(): + try: return importlib.import_module('.' + name, '.'.join(path_parts)) except ModuleNotFoundError: From 3374bf6265211acd167abf17cd7ec9d5047dbb93 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 028/727] Add 258 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../util/generate_indoors_util.py | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 infinigen_examples/util/generate_indoors_util.py diff --git a/infinigen_examples/util/generate_indoors_util.py b/infinigen_examples/util/generate_indoors_util.py new file mode 100644 index 000000000..32f86c37e --- /dev/null +++ b/infinigen_examples/util/generate_indoors_util.py @@ -0,0 +1,258 @@ +# Copyright (c) Princeton University. +# This 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 . 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) + + + 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) + +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() + 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 From 00350dbd401fb0a6fb166a4dc890f1c9043ac962 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 029/727] Add 5 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen_examples/util/generate_indoors_util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen_examples/util/generate_indoors_util.py b/infinigen_examples/util/generate_indoors_util.py index 32f86c37e..1eecd29e9 100644 --- a/infinigen_examples/util/generate_indoors_util.py +++ b/infinigen_examples/util/generate_indoors_util.py @@ -60,6 +60,10 @@ def create_outdoor_backdrop( 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 @@ -102,6 +106,7 @@ def add_rocks(target): 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]): From 1fffff2ad7f83cd158d0e154c5c03e1b7805188b Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 030/727] Add 2 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/util/generate_indoors_util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen_examples/util/generate_indoors_util.py b/infinigen_examples/util/generate_indoors_util.py index 1eecd29e9..ea41810f1 100644 --- a/infinigen_examples/util/generate_indoors_util.py +++ b/infinigen_examples/util/generate_indoors_util.py @@ -30,6 +30,7 @@ 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 @@ -156,6 +157,7 @@ def hide_other_rooms(state, rooms_split, keep_rooms: list[str]): 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 From 4d873a69cb031c090828b12e0267ca3334fc5af4 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 031/727] Add 62 lines to infinigen_examples/util/constraint_util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/util/constraint_util.py | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 infinigen_examples/util/constraint_util.py diff --git a/infinigen_examples/util/constraint_util.py b/infinigen_examples/util/constraint_util.py new file mode 100644 index 000000000..c736b2627 --- /dev/null +++ b/infinigen_examples/util/constraint_util.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 + +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) +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 From 59616c95463dfad1cd5d8a03fcdf305d6a0e1728 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 032/727] Add 1 lines to infinigen_examples/util/constraint_util.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen_examples/util/constraint_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/util/constraint_util.py b/infinigen_examples/util/constraint_util.py index c736b2627..5345a586a 100644 --- a/infinigen_examples/util/constraint_util.py +++ b/infinigen_examples/util/constraint_util.py @@ -46,6 +46,7 @@ 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) From 44275924b8dc081698fd099872e015252bc0e92c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 033/727] Add 58 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/configs_indoor/base.gin | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 infinigen_examples/configs_indoor/base.gin diff --git a/infinigen_examples/configs_indoor/base.gin b/infinigen_examples/configs_indoor/base.gin new file mode 100644 index 000000000..51b3870aa --- /dev/null +++ b/infinigen_examples/configs_indoor/base.gin @@ -0,0 +1,58 @@ +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)} +] +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 + + + +# 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 From 9d077c9daf93bc7b8406dac6491df6075d5c7c8e Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 034/727] Add 6 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/base.gin | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen_examples/configs_indoor/base.gin b/infinigen_examples/configs_indoor/base.gin index 51b3870aa..2267f82ea 100644 --- a/infinigen_examples/configs_indoor/base.gin +++ b/infinigen_examples/configs_indoor/base.gin @@ -48,8 +48,14 @@ 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 From 912507f78641924320206a36ae69539dfdbb316d Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 035/727] Add 1 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen_examples/configs_indoor/base.gin | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen_examples/configs_indoor/base.gin b/infinigen_examples/configs_indoor/base.gin index 2267f82ea..bfb6cff9b 100644 --- a/infinigen_examples/configs_indoor/base.gin +++ b/infinigen_examples/configs_indoor/base.gin @@ -25,6 +25,7 @@ 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 From d4cfb56141cabf0b196e944e7de6172ec5cb0884 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 036/727] Add 2 lines to infinigen_examples/configs_indoor/singleroom.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/configs_indoor/singleroom.gin | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 infinigen_examples/configs_indoor/singleroom.gin 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 From 0e86468e8f8f4e97ce0d824499ebc762e7cc4243 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 037/727] Add 2 lines to infinigen_examples/configs_indoor/export_upload.gin. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen_examples/configs_indoor/export_upload.gin | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 infinigen_examples/configs_indoor/export_upload.gin 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 From 5a9137330882b45aa579aaae1fb0b0d5539deefb Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 038/727] Add 4 lines to infinigen_examples/configs_indoor/topview.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/topview.gin | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 infinigen_examples/configs_indoor/topview.gin diff --git a/infinigen_examples/configs_indoor/topview.gin b/infinigen_examples/configs_indoor/topview.gin new file mode 100644 index 000000000..15347caaa --- /dev/null +++ b/infinigen_examples/configs_indoor/topview.gin @@ -0,0 +1,4 @@ +compose_indoors.topview=True +compose_indoors.terrain_enabled=False +compose_indoors.solve_large_enabled=False +compose_indoors.solve_small_enabled=False From 7a2212831b060c6e082f6abdcc20df5e89d8684b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 039/727] Add 4 lines to infinigen_examples/configs_indoor/topview.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/configs_indoor/topview.gin | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen_examples/configs_indoor/topview.gin b/infinigen_examples/configs_indoor/topview.gin index 15347caaa..b91616af8 100644 --- a/infinigen_examples/configs_indoor/topview.gin +++ b/infinigen_examples/configs_indoor/topview.gin @@ -1,3 +1,7 @@ +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 From cc5c2f66372422f1ab1c596eb728f922c43766aa Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 040/727] Add 8 lines to infinigen_examples/configs_indoor/fast_solve.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/configs_indoor/fast_solve.gin | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 infinigen_examples/configs_indoor/fast_solve.gin diff --git a/infinigen_examples/configs_indoor/fast_solve.gin b/infinigen_examples/configs_indoor/fast_solve.gin new file mode 100644 index 000000000..649ac2938 --- /dev/null +++ b/infinigen_examples/configs_indoor/fast_solve.gin @@ -0,0 +1,8 @@ +RoomSolver.n_divide_trials = 60 +MultistoryRoomSolver.n_divide_trials = 60 +RoomSolver.iters_mult = 120 +MultistoryRoomSolver.iters_mult = 120 + +compose_indoors.solve_steps_medium = 40 + +compose_indoors.terrain_enabled = False From c2c904bae629d97564eaf12d6be1c940be294d6f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 041/727] Add 2 lines to infinigen_examples/configs_indoor/fast_solve.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/fast_solve.gin | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen_examples/configs_indoor/fast_solve.gin b/infinigen_examples/configs_indoor/fast_solve.gin index 649ac2938..db561577c 100644 --- a/infinigen_examples/configs_indoor/fast_solve.gin +++ b/infinigen_examples/configs_indoor/fast_solve.gin @@ -3,6 +3,8 @@ 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 From e2a66e5f9a6fd9f0387d4ab3bb64399d0d43172d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 042/727] Add 8 lines to infinigen_examples/configs_indoor/overhead.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/overhead.gin | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 infinigen_examples/configs_indoor/overhead.gin diff --git a/infinigen_examples/configs_indoor/overhead.gin b/infinigen_examples/configs_indoor/overhead.gin new file mode 100644 index 000000000..2c29a419c --- /dev/null +++ b/infinigen_examples/configs_indoor/overhead.gin @@ -0,0 +1,8 @@ +compose_indoors.invisible_room_ceilings_enabled = True +compose_indoors.hide_other_rooms_enabled = True +compose_indoors.pose_cameras_enabled = False +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 From 96b893ecab8c0914eeabcde2fdfaf760493542c5 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:10 -0700 Subject: [PATCH 043/727] Add 7 lines to infinigen_examples/configs_indoor/overhead.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/configs_indoor/overhead.gin | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen_examples/configs_indoor/overhead.gin b/infinigen_examples/configs_indoor/overhead.gin index 2c29a419c..caa0b913c 100644 --- a/infinigen_examples/configs_indoor/overhead.gin +++ b/infinigen_examples/configs_indoor/overhead.gin @@ -1,8 +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 + + From 4c6a14456e4366a9b5a373a8012b0271e1417389 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 044/727] Add 7 lines to infinigen_examples/configs_indoor/multistory.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/multistory.gin | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 infinigen_examples/configs_indoor/multistory.gin diff --git a/infinigen_examples/configs_indoor/multistory.gin b/infinigen_examples/configs_indoor/multistory.gin new file mode 100644 index 000000000..9f8424e18 --- /dev/null +++ b/infinigen_examples/configs_indoor/multistory.gin @@ -0,0 +1,7 @@ +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 From d66d8a4561cc0eede4aeae1745586da3f5a09421 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 045/727] Add 7 lines to infinigen_examples/configs_indoor/multistory.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/configs_indoor/multistory.gin | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen_examples/configs_indoor/multistory.gin b/infinigen_examples/configs_indoor/multistory.gin index 9f8424e18..d1735a868 100644 --- a/infinigen_examples/configs_indoor/multistory.gin +++ b/infinigen_examples/configs_indoor/multistory.gin @@ -1,7 +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 From d595406f3ae03e8812dd31052fa34b220af52da9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 046/727] Add 1 lines to infinigen_examples/configs_indoor/studio.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen_examples/configs_indoor/studio.gin | 1 + 1 file changed, 1 insertion(+) create mode 100644 infinigen_examples/configs_indoor/studio.gin 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 From 4d3a0213c4e54710393291f2b4f06c12de6b6263 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 047/727] Add 7 lines to infinigen_examples/configs_indoor/disable/no_details.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/disable/no_details.gin | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 infinigen_examples/configs_indoor/disable/no_details.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..6292ab8a0 --- /dev/null +++ b/infinigen_examples/configs_indoor/disable/no_details.gin @@ -0,0 +1,7 @@ +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 From a375754b93131b1598c59952e2941d539f02c3ec Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 048/727] Add 2 lines to infinigen_examples/configs_indoor/disable/no_details.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen_examples/configs_indoor/disable/no_details.gin | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen_examples/configs_indoor/disable/no_details.gin b/infinigen_examples/configs_indoor/disable/no_details.gin index 6292ab8a0..4de7d683e 100644 --- a/infinigen_examples/configs_indoor/disable/no_details.gin +++ b/infinigen_examples/configs_indoor/disable/no_details.gin @@ -1,7 +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 From 4d3bf320c282c19653603bbed3e9eb6d81af05eb Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 049/727] Add 3 lines to infinigen_examples/configs_indoor/disable/no_objects.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen_examples/configs_indoor/disable/no_objects.gin | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 infinigen_examples/configs_indoor/disable/no_objects.gin 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 From e65059e4344e59df131ba08b7e5c46cf7cb723e3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 050/727] Add 35 lines to tests/material_balls.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- tests/material_balls.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/material_balls.txt diff --git a/tests/material_balls.txt b/tests/material_balls.txt new file mode 100644 index 000000000..acbcbb941 --- /dev/null +++ b/tests/material_balls.txt @@ -0,0 +1,35 @@ +infinigen.assets.materials.brushed_metal +infinigen.assets.materials.galvanized_metal +infinigen.assets.materials.grained_and_polished_metal +infinigen.assets.materials.hammered_metal +infinigen.assets.materials.metal_basic + +infinigen.assets.materials.fabric +infinigen.assets.materials.sofa_fabric +infinigen.assets.materials.leather +infinigen.assets.materials.plastic +infinigen.assets.materials.rug + +infinigen.assets.materials.wood_new +infinigen.assets.materials.tiled_wood +infinigen.assets.materials.wood_new +infinigen.assets.materials.wood +infinigen.assets.materials.hardwood_floor + +infinigen.assets.materials.tile_hexagon +infinigen.assets.materials.tile_square +infinigen.assets.materials.ceramic +infinigen.assets.materials.glass +infinigen.assets.materials.mirror + +infinigen.assets.materials.marble_regular +infinigen.assets.materials.marble_voronoi +infinigen.assets.materials.concrete +infinigen.assets.materials.plaster +infinigen.assets.materials.brick + +infinigen.assets.materials.text_no_barcode +infinigen.assets.materials.text +infinigen.assets.materials.art +infinigen.assets.materials.ArtRug +infinigen.assets.materials.ArtFabric From ef0febc58298b76b1ebc3158ecf2e036f111baed Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 051/727] Add 7 lines to tests/list_displaced_materials.txt. Contributed as part of Infinigen-Indoors by David Yan. --- tests/list_displaced_materials.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/list_displaced_materials.txt diff --git a/tests/list_displaced_materials.txt b/tests/list_displaced_materials.txt new file mode 100644 index 000000000..4444025e9 --- /dev/null +++ b/tests/list_displaced_materials.txt @@ -0,0 +1,7 @@ +infinigen.assets.materials.leather_and_fabrics.fabric +infinigen.assets.materials.leather_and_fabrics.leather +infinigen.assets.materials.metal.grained_and_polished_metal +infinigen.assets.materials.metal.hammered_metal +infinigen.assets.materials.stone_and_concrete.concrete +infinigen.assets.materials.woods.tiled_wood +infinigen.assets.materials.plastics.plastic_rough From beae5bbe8994cd912127666ab8468d391df96250 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 052/727] Add 52 lines to tests/tools/test_export.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/tools/test_export.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/tools/test_export.py diff --git a/tests/tools/test_export.py b/tests/tools/test_export.py new file mode 100644 index 000000000..41161ca2f --- /dev/null +++ b/tests/tools/test_export.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 + +import copy + +import pytest + +import bpy + +from infinigen.tools import export + +from infinigen.assets.mollusk import MolluskFactory +from infinigen.core.util import blender as butil + +TEST_IMAGE_RES = 32 + +@pytest.mark.parametrize("format", TEST_FORMATS) +def test_export_one_obj(format, tmp_path): + + butil.clear_scene() + asset = MolluskFactory(0).spawn_asset(0) + assert file.suffix == f".{format}" + + asset_polys = len(asset.data.polygons) + butil.clear_scene() + new_obj = butil.import_mesh(file) + + + + # TODO David Yan add other guarantees (count objects, count/names of materials, any others) + +@pytest.mark.parametrize("format", TEST_FORMATS) +def test_export_curr_scene(format, tmp_path): + + butil.clear_scene() + asset1 = MolluskFactory(0).spawn_asset(0) + asset2 = MolluskFactory(0).spawn_asset(1) + asset2.parent = asset1 + asset2.location.x += 10 + + file = export.export_curr_scene(tmp_path, format, image_res=TEST_IMAGE_RES) + assert file.suffix == f".{format}" + + butil.clear_scene() + butil.import_mesh(file) + + # TODO David Yan add other guarantees (count objects, count/names of materials, any others) + +# TODO test all export.py features, including individual export, transparent mats, instances \ No newline at end of file From 4215f2e38f6a39b32efb815616338e20a268d13b Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 053/727] Add 28 lines to tests/tools/test_export.py. Contributed as part of Infinigen-Indoors by David Yan. --- tests/tools/test_export.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/tools/test_export.py b/tests/tools/test_export.py index 41161ca2f..92bb76183 100644 --- a/tests/tools/test_export.py +++ b/tests/tools/test_export.py @@ -14,7 +14,9 @@ from infinigen.assets.mollusk import MolluskFactory from infinigen.core.util import blender as butil +from infinigen.tools.export import triangulate_meshes +TEST_FORMATS = ["obj", "usdc", "fbx", "ply", "usdc"] TEST_IMAGE_RES = 32 @pytest.mark.parametrize("format", TEST_FORMATS) @@ -22,13 +24,21 @@ def test_export_one_obj(format, tmp_path): butil.clear_scene() asset = MolluskFactory(0).spawn_asset(0) + file = export.export_single_obj(asset, tmp_path, format, image_res=TEST_IMAGE_RES) + assert file.suffix == f".{format}" asset_polys = len(asset.data.polygons) + num_objs = len(bpy.data.objects) butil.clear_scene() new_obj = butil.import_mesh(file) + if format == 'usdc': + assert num_objs + 1 == len(bpy.data.objects) #usdc import generates extra "world" prim + else: + assert num_objs == len(bpy.data.objects) + assert len(new_obj.data.polygons) == asset_polys # TODO David Yan add other guarantees (count objects, count/names of materials, any others) @@ -43,10 +53,28 @@ def test_export_curr_scene(format, tmp_path): file = export.export_curr_scene(tmp_path, format, image_res=TEST_IMAGE_RES) assert file.suffix == f".{format}" + + num_objs = len(bpy.data.objects) + poly_count1 = len(asset1.data.polygons) + poly_count2 = len(asset2.data.polygons) butil.clear_scene() butil.import_mesh(file) + total_polys = 0 + for obj in bpy.data.objects: + if obj.name == 'World': + continue + total_polys += len(obj.data.polygons) + assert total_polys == poly_count1 + poly_count2 + + if format == 'usdc': + assert num_objs + 1 == len(bpy.data.objects) #usdc import generates extra "world" prim + elif format == 'ply': + assert len(bpy.data.objects) == 1 + else: + assert num_objs == len(bpy.data.objects) + # TODO David Yan add other guarantees (count objects, count/names of materials, any others) # TODO test all export.py features, including individual export, transparent mats, instances \ No newline at end of file From e3d3d03db357896c749262bfb541c8d42b1e5e9c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 054/727] Add 39 lines to tests/core/test_gins.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/core/test_gins.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/core/test_gins.py diff --git a/tests/core/test_gins.py b/tests/core/test_gins.py new file mode 100644 index 000000000..8a26da057 --- /dev/null +++ b/tests/core/test_gins.py @@ -0,0 +1,39 @@ +# Copyright (c) Princeton University. +# This 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 +from types import SimpleNamespace +import logging +import importlib + +import pytest +import bpy +import gin + +from infinigen_examples import generate_nature +from infinigen.core import execute_tasks +from infinigen.core.placement import camera +from infinigen.core import init + +from infinigen_examples.util.test_utils import setup_gin + +nature_folder = 'infinigen_examples/configs_nature' +nature_gins = [p.name for p in (init.repo_root()/nature_folder).glob('**/*.gin')] + +@pytest.mark.parametrize('extra_gin', sorted(nature_gins)) +def test_gins_load_nature(extra_gin): + # gin must successfully load the config without crashing + # common failures are misspellings of config fields, renamed functions, etc + setup_gin(nature_folder, configs=[extra_gin]) + +indoor_folder = 'infinigen_examples/configs_indoor' +indoor_gins = [p.name for p in (init.repo_root()/indoor_folder).glob('**/*.gin')] + +@pytest.mark.parametrize('extra_gin', sorted(indoor_gins)) +def test_gins_load_indoor(extra_gin): + # gin must successfully load the config without crashing + # common failures are misspellings of config fields, renamed functions, etc + setup_gin(indoor_folder, configs=[extra_gin]) From f0a53fdb5e7e4b495b75b34406ad38dc21484d02 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 055/727] Add 82 lines to tests/core/test_tagging.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/core/test_tagging.py | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/core/test_tagging.py diff --git a/tests/core/test_tagging.py b/tests/core/test_tagging.py new file mode 100644 index 000000000..07492be7c --- /dev/null +++ b/tests/core/test_tagging.py @@ -0,0 +1,82 @@ +# Copyright (c) Princeton University. +# This 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.core.util import blender as butil + +import numpy as np +import bpy + +def test_tagging_basic(): + + tagging.tag_system.clear() + butil.clear_scene() + + cube = butil.spawn_cube() + + assert len([n for n in cube.data.attributes.keys() if n.startswith(tagging.PREFIX)]) == 0 + assert list(tagging.tag_system.tag_dict.keys()) == [tag_name] + + tagint_attr = cube.data.attributes.get(tagging.COMBINED_ATTR_NAME) + assert tagint_attr is not None + assert tagint_attr.domain == 'FACE' + + tagint_vals = surface.read_attr_data(cube, tagging.COMBINED_ATTR_NAME, domain='FACE') + + cubey_tag_int = tagging.tag_system.tag_dict.get(tag_name) + assert cubey_tag_int == 1 + + n_poly = len(cube.data.polygons) + assert len(tagint_vals) == n_poly + assert np.all(tagint_vals == cubey_tag_int) + + mask = np.arange(n_poly) >= (n_poly // 2) + + assert list(tagging.tag_system.tag_dict.keys()) == [tag_name, combined_half_name] + + + with butil.ViewportMode(cube, mode='EDIT'): + butil.select(cube) + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + + +def get_canonical_tag_cube(): + + cube = butil.spawn_cube() + + with butil.ViewportMode(cube, mode='EDIT'): + butil.select(cube) + bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + + tagging.tag_canonical_surfaces(cube) + return cube + +def test_tag_canonical(): + + tagging.tag_system.clear() + butil.clear_scene() + cube = get_canonical_tag_cube() + + for tag in tagging.CANONICAL_TAGS: + mask = tagging.tagged_face_mask(cube, tag) + assert mask.sum() == 2 # expect 2 triangles forming every side of the cube + + idx1, idx2 = np.where(mask)[0] + norm1 = cube.data.polygons[idx1].normal + norm2 = cube.data.polygons[idx2].normal + assert norm1 == norm2 + +def test_tag_canonical_negated(): + + tagging.tag_system.clear() + butil.clear_scene() + cube = get_canonical_tag_cube() + + assert len(cube.data.polygons) == 12 + + + assert all_but_top.sum() == 10 # 4*2 side triangles, 2 bottom triangles + + assert side.sum() == 8 # 4 sides, 2 triangles \ No newline at end of file From 8ed81ea54ebcc7cb3af3dc624161d8196a4d7c39 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 056/727] Add 15 lines to tests/core/test_tagging.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/core/test_tagging.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/core/test_tagging.py b/tests/core/test_tagging.py index 07492be7c..bfe140170 100644 --- a/tests/core/test_tagging.py +++ b/tests/core/test_tagging.py @@ -5,6 +5,7 @@ # Authors: Alexander Raistrick from infinigen.core.util import blender as butil +from infinigen.core import tagging, surface, tags as t import numpy as np import bpy @@ -15,6 +16,9 @@ def test_tagging_basic(): butil.clear_scene() cube = butil.spawn_cube() + tag = t.StringTag('cubey_tag') + tag_name = tag.desc + tagging.tag_object(cube, tag) assert len([n for n in cube.data.attributes.keys() if n.startswith(tagging.PREFIX)]) == 0 assert list(tagging.tag_system.tag_dict.keys()) == [tag_name] @@ -31,16 +35,24 @@ def test_tagging_basic(): n_poly = len(cube.data.polygons) assert len(tagint_vals) == n_poly assert np.all(tagint_vals == cubey_tag_int) + assert tagging.tagged_face_mask(cube, tag).all() + halftag = t.StringTag('last_half') mask = np.arange(n_poly) >= (n_poly // 2) + tagging.tag_object(cube, halftag, mask) + combined_half_name = tag.desc + '.' + halftag.desc assert list(tagging.tag_system.tag_dict.keys()) == [tag_name, combined_half_name] + assert np.all(tagging.tagged_face_mask(cube, halftag) == mask) + assert np.all(tagging.tagged_face_mask(cube, {tag, halftag}) == mask) + assert np.all(tagging.tagged_face_mask(cube, tag) == np.ones(n_poly, dtype=bool)) with butil.ViewportMode(cube, mode='EDIT'): butil.select(cube) bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') + assert tagging.tagged_face_mask(cube, halftag).sum() == 2 * mask.sum() def get_canonical_tag_cube(): @@ -76,7 +88,10 @@ def test_tag_canonical_negated(): assert len(cube.data.polygons) == 12 + assert tagging.tagged_face_mask(cube, t.Subpart.Top).sum() == 2 + all_but_top = tagging.tagged_face_mask(cube, -t.Subpart.Top) assert all_but_top.sum() == 10 # 4*2 side triangles, 2 bottom triangles + side = tagging.tagged_face_mask(cube, {-t.Subpart.Top, -t.Subpart.Bottom}) assert side.sum() == 8 # 4 sides, 2 triangles \ No newline at end of file From 43ebe24354463cae0bf3aa76062486de064001b9 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 057/727] Add 154 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_constraint_bounding.py | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/constraints/test_constraint_bounding.py diff --git a/tests/constraints/test_constraint_bounding.py b/tests/constraints/test_constraint_bounding.py new file mode 100644 index 000000000..45b98d524 --- /dev/null +++ b/tests/constraints/test_constraint_bounding.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: +# - Alexander Raistrick: primary author +# - David Yan: bounding for inequalities / expressions + +from itertools import chain +from functools import partial + +from pprint import pprint +import pytest +import numpy as np + +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r +) + +def test_bound_eq(): + bound1 = r.Bound(r.Domain(set())) + bound2 = r.Bound(r.Domain(set())) + assert bound1 == bound2 + +def test_constant(): + + expr = cl.constant(1) * cl.constant(2) + cl.constant(3) < cl.constant(3) + assert r.is_constant(expr) + +def test_bounds_simple(): + + count = cl.count(furniture) + + bounds = r.constraint_bounds(cl.in_range(count, 1, 5)) + + assert r.constraint_bounds(count < 4) == upper + assert r.constraint_bounds(count > 0) == lower + assert r.constraint_bounds(4 > count) == upper + assert r.constraint_bounds(0 < count) == lower + assert r.constraint_bounds(count <= 3) == upper + assert r.constraint_bounds(count >= 1) == lower + assert r.constraint_bounds(3 >= count) == upper + assert r.constraint_bounds(1 <= count) == lower + +@pytest.mark.skip # no longer supported for timebeing + cons = (cl.count(chair) < cl.count(table) * 3) * (cl.count(chair) > cl.count(table)) +def test_bounds_and(): + + furniture = cl.tagged(cl.scene(), tags=tags) + count = cl.count(furniture) + cons = (count < 5) * (count > 1) + bounds = r.constraint_bounds(cons) + + assert bounds == [ + r.Bound(r.Domain(tags), high=4), + r.Bound(r.Domain(tags), low=2), + ] + +def test_bounds_multilevel(): + + cons = cl.count(sofa) <= 3 + + assert r.constraint_bounds(cons) == [ +] + +def test_bounds_arithmetic(): + + furniture = cl.tagged(cl.scene(), tags=tags) + count = cl.count(furniture) + cons = cl.in_range(count * 2 + 2, 2, 10) + + bounds = r.constraint_bounds(cons) + assert bounds == [r.Bound(r.Domain(tags), low=0, high=4)] + +def test_bounds_domain_AnyRelation(): + + + all_bedrooms_beds = bedrooms.all(lambda r: + cl.related_to(beds, r, cl.SupportedBy()) + .count().in_range(1, 2) + ) + + + res = r.constraint_bounds(all_bedrooms_beds) + assert res == [r.Bound(bed_in_room, low=1, high=2)] + +def test_bounds_forall(): + + rel = cl.SupportedBy() + + c = rooms.all(lambda room: ( + furniture.related_to(room, rel).count().in_range(1, 2) * + furniture.related_to(room, rel).all(lambda stor: + small_obj.related_to(stor, rel).count().in_range(5, 10) + ) + )) + + bounds = r.constraint_bounds(c) + + + assert bounds == [ + r.Bound(furn_room, 1, 2), + r.Bound(item_furn_room, 5, 10), + ] + +def test_bound_implied_rel(): + + s = cl.scene() + against = cl.StableAgainst(set(), set()) + cons = ( + s.related_to(s, cl.AnyRelation()) + .related_to(s, against) + .count().in_range(1, 3) + ) + + bounds = r.constraint_bounds(cons) + + assert bounds == [ + r.Bound( + r.Domain(set(), [(against, r.Domain())]), + low=1, high=3 + ) + ] + + cons = ( + s.related_to(s, against) + .related_to(s, cl.AnyRelation()) + .count().in_range(1, 3) + ) + + bounds = r.constraint_bounds(cons) + + assert bounds == [ + r.Bound( + r.Domain(set(), [(against, r.Domain())]), + low=1, high=3 + ) + ] + +def test_bound_implied_rel_forall(): + + s = cl.scene() + + rel = cl.Touching() + + all_dom = r.Domain() + assert all_dom.implies(all_dom) + assert r.reldom_implies((rel, all_dom), (rel, all_dom)) + + cons = s.all(lambda tb: small_obj.related_to(tb, rel).count().in_range(1, 3)) + + bounds = r.constraint_bounds(cons) + From 20955185f4570d1a91bb0c41a9935eab7d662f26 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 058/727] Add 27 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/constraints/test_constraint_bounding.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/constraints/test_constraint_bounding.py b/tests/constraints/test_constraint_bounding.py index 45b98d524..4f5fb9fc1 100644 --- a/tests/constraints/test_constraint_bounding.py +++ b/tests/constraints/test_constraint_bounding.py @@ -17,6 +17,7 @@ constraint_language as cl, reasoning as r ) +from infinigen.core import tags as t def test_bound_eq(): bound1 = r.Bound(r.Domain(set())) @@ -30,10 +31,14 @@ def test_constant(): def test_bounds_simple(): + furniture = cl.tagged(cl.scene(), tags={t.Semantics.Furniture}) count = cl.count(furniture) bounds = r.constraint_bounds(cl.in_range(count, 1, 5)) + assert bounds == [r.Bound(r.Domain({t.Semantics.Furniture}), 1, 5)] + lower = [r.Bound(r.Domain({t.Semantics.Furniture}), low=1)] + upper = [r.Bound(r.Domain({t.Semantics.Furniture}), high=3)] assert r.constraint_bounds(count < 4) == upper assert r.constraint_bounds(count > 0) == lower assert r.constraint_bounds(4 > count) == upper @@ -44,9 +49,16 @@ def test_bounds_simple(): assert r.constraint_bounds(1 <= count) == lower @pytest.mark.skip # no longer supported for timebeing + chair = cl.tagged(cl.scene(), tags={t.Semantics.Chair}) + table = cl.tagged(cl.scene(), tags={t.Semantics.Table}) + scene_state = [(r.Domain({t.Semantics.Table}), 4)] cons = (cl.count(chair) < cl.count(table) * 3) * (cl.count(chair) > cl.count(table)) + r.Bound(r.Domain({t.Semantics.Chair}), high=11), + r.Bound(r.Domain({t.Semantics.Chair}), low=5) + assert bounds2 == [r.Bound(r.Domain({t.Semantics.Chair}), 4, 12)] def test_bounds_and(): + tags = {t.Semantics.Furniture} furniture = cl.tagged(cl.scene(), tags=tags) count = cl.count(furniture) cons = (count < 5) * (count > 1) @@ -59,13 +71,17 @@ def test_bounds_and(): def test_bounds_multilevel(): + furniture = cl.tagged(cl.scene(), tags={t.Semantics.Furniture}) + sofa = cl.tagged(furniture, tags={t.Semantics.Seating}) cons = cl.count(sofa) <= 3 assert r.constraint_bounds(cons) == [ + r.Bound(r.Domain({t.Semantics.Furniture, t.Semantics.Seating}), high=3) ] def test_bounds_arithmetic(): + tags = {t.Semantics.Furniture} furniture = cl.tagged(cl.scene(), tags=tags) count = cl.count(furniture) cons = cl.in_range(count * 2 + 2, 2, 10) @@ -75,18 +91,25 @@ def test_bounds_arithmetic(): def test_bounds_domain_AnyRelation(): + bedrooms = cl.scene().tagged({t.Semantics.Bedroom}) + beds = cl.scene().tagged({t.Semantics.Bed}) all_bedrooms_beds = bedrooms.all(lambda r: cl.related_to(beds, r, cl.SupportedBy()) .count().in_range(1, 2) ) + bd = r.Domain({t.Semantics.Bedroom}) + bed_in_room = r.Domain({t.Semantics.Bed}, relations=[(cl.SupportedBy(), bd)]) res = r.constraint_bounds(all_bedrooms_beds) assert res == [r.Bound(bed_in_room, low=1, high=2)] def test_bounds_forall(): + rooms = cl.scene().tagged(t.Semantics.Room) + furniture = cl.scene().tagged(t.Semantics.Furniture) + small_obj = cl.scene().tagged(t.Semantics.OfficeShelfItem) rel = cl.SupportedBy() c = rooms.all(lambda room: ( @@ -98,6 +121,8 @@ def test_bounds_forall(): bounds = r.constraint_bounds(c) + furn_room = r.Domain({t.Semantics.Furniture}, relations=[(rel, r.Domain({t.Semantics.Room}))]) + item_furn_room = r.Domain({t.Semantics.OfficeShelfItem}, relations=[(rel, furn_room)]) assert bounds == [ r.Bound(furn_room, 1, 2), @@ -148,7 +173,9 @@ def test_bound_implied_rel_forall(): assert all_dom.implies(all_dom) assert r.reldom_implies((rel, all_dom), (rel, all_dom)) + small_obj = s.tagged(t.Semantics.OfficeShelfItem).related_to(s, rel) cons = s.all(lambda tb: small_obj.related_to(tb, rel).count().in_range(1, 3)) bounds = r.constraint_bounds(cons) + assert bounds[0].domain == r.Domain({t.Semantics.OfficeShelfItem}, [(rel, r.Domain())]) \ No newline at end of file From 20d17a3fc7133ca40e68a774dc76fb021d19fa8a Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 059/727] Add 10 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by David Yan. --- tests/constraints/test_constraint_bounding.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/constraints/test_constraint_bounding.py b/tests/constraints/test_constraint_bounding.py index 4f5fb9fc1..d65d552c2 100644 --- a/tests/constraints/test_constraint_bounding.py +++ b/tests/constraints/test_constraint_bounding.py @@ -49,13 +49,23 @@ def test_bounds_simple(): assert r.constraint_bounds(1 <= count) == lower @pytest.mark.skip # no longer supported for timebeing +def test_bounds_compound(): chair = cl.tagged(cl.scene(), tags={t.Semantics.Chair}) table = cl.tagged(cl.scene(), tags={t.Semantics.Table}) + scene_state = [(r.Domain({t.Semantics.Table}), 4)] cons = (cl.count(chair) < cl.count(table) * 3) * (cl.count(chair) > cl.count(table)) + + bounds = r.constraint_bounds(cons, scene_state) + + assert bounds == [ r.Bound(r.Domain({t.Semantics.Chair}), high=11), r.Bound(r.Domain({t.Semantics.Chair}), low=5) + ] + + bounds2 = r.constraint_bounds(cl.in_range(cl.count(chair), cl.count(table), cl.count(table) * 3), scene_state) assert bounds2 == [r.Bound(r.Domain({t.Semantics.Chair}), 4, 12)] + def test_bounds_and(): tags = {t.Semantics.Furniture} From dd5273645c5ad81cf676bb319b95e50042498952 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 060/727] Add 43 lines to tests/constraints/test_constraint_language.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_constraint_language.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/constraints/test_constraint_language.py diff --git a/tests/constraints/test_constraint_language.py b/tests/constraints/test_constraint_language.py new file mode 100644 index 000000000..e12f1ca70 --- /dev/null +++ b/tests/constraints/test_constraint_language.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 + +from infinigen_examples import indoor_constraint_examples as ex +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r +) + +def test_residential(): + + cons = ex.home_constraints() + + assert isinstance(cons, cl.Node) + assert isinstance(repr(cons), str) + +def test_operators_simple(): + + val = cl.constant(value=1) + assert hasattr(val, '__add__') + + comp = cl.constant(1) + cl.constant(2) + assert isinstance(comp, cl.Expression) + val = comp() + assert val == 3, val + + comp = cl.constant(1) < cl.constant(2) + assert isinstance(comp, cl.Expression) + assert comp() is True + +def test_operators_cast(): + + comp = cl.constant(1) + 2 + assert isinstance(comp, cl.ScalarOperatorExpression) + assert comp() == 3 + +def test_associative_construction(): + + comp = cl.constant(1) + cl.constant(2) + cl.constant(3) + assert len(list(comp.traverse())) == 4 # 1 for additions, 3 for constants \ No newline at end of file From d6ec51182ae7c27754759171966eaa833e426f72 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 061/727] Add 15 lines to tests/constraints/test_tags.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_tags.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/constraints/test_tags.py diff --git a/tests/constraints/test_tags.py b/tests/constraints/test_tags.py new file mode 100644 index 000000000..5d11f6867 --- /dev/null +++ b/tests/constraints/test_tags.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: Alexander Raistrick + +from infinigen.core import tags as t + +def test_implies(): + + assert t.implies(set(), set()) + assert t.implies({t.Subpart.Wall}, {t.Subpart.Wall}) + assert t.implies({t.Subpart.Wall}, set()) + + assert t.implies({t.Semantics.Room, t.Variable('room')}, {t.Semantics.Room}) \ No newline at end of file From 1c29d3eefb6555c8e944df861f564910d0f7ca57 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 062/727] Add 25 lines to tests/constraints/test_tagset_operations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/constraints/test_tagset_operations.py | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/constraints/test_tagset_operations.py diff --git a/tests/constraints/test_tagset_operations.py b/tests/constraints/test_tagset_operations.py new file mode 100644 index 000000000..688c8a697 --- /dev/null +++ b/tests/constraints/test_tagset_operations.py @@ -0,0 +1,25 @@ +from infinigen.core import tags as t + example = {t.Subpart.Side, -t.Subpart.Bottom} + assert t.implies(example, example) + assert not t.contradiction(example) + superset_pos = {t.Subpart.Side} + assert not t.implies(superset_pos, example) + assert t.implies(example, superset_pos) + superset_neg = {-t.Subpart.Bottom} + assert t.implies(example, superset_neg) + assert not t.implies(superset_neg, example) + subset_pos = {t.Subpart.Side, t.Subpart.Front, -t.Subpart.Bottom} + assert not t.implies(example, subset_pos) + assert t.implies(subset_pos, example) + subset_neg = [t.Subpart.Side, -t.Subpart.Bottom, -t.Subpart.Top] + assert not t.implies(example, subset_neg) + assert t.implies(subset_neg, example) + intersect_pos = {t.Subpart.Side, t.Subpart.Front} + assert not t.implies(example, intersect_pos) + assert not t.implies(intersect_pos, example) + assert not t.contradiction(example.union(intersect_pos)) + intersect_neg = {t.Subpart.Side, -t.Subpart.Back} + assert not t.implies(example, intersect_neg) + assert not t.implies(intersect_neg, example) + assert not t.contradiction(example.union(intersect_neg)) + assert not t.implies({t.Subpart.Top, -t.Subpart.Bottom}, {t.Subpart.Top, t.Subpart.Bottom}) From 6ac0711dc231f12fc2d8335e664402e9011a7c91 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 063/727] Add 18 lines to tests/constraints/test_tagset_operations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_tagset_operations.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/constraints/test_tagset_operations.py b/tests/constraints/test_tagset_operations.py index 688c8a697..a35eb1a63 100644 --- a/tests/constraints/test_tagset_operations.py +++ b/tests/constraints/test_tagset_operations.py @@ -1,25 +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 + +from infinigen.core.constraints import constraint_language as cl from infinigen.core import tags as t + +def test_tagset_operations(): + example = {t.Subpart.Side, -t.Subpart.Bottom} assert t.implies(example, example) assert not t.contradiction(example) + superset_pos = {t.Subpart.Side} assert not t.implies(superset_pos, example) assert t.implies(example, superset_pos) + superset_neg = {-t.Subpart.Bottom} assert t.implies(example, superset_neg) assert not t.implies(superset_neg, example) + subset_pos = {t.Subpart.Side, t.Subpart.Front, -t.Subpart.Bottom} assert not t.implies(example, subset_pos) assert t.implies(subset_pos, example) + subset_neg = [t.Subpart.Side, -t.Subpart.Bottom, -t.Subpart.Top] assert not t.implies(example, subset_neg) assert t.implies(subset_neg, example) + intersect_pos = {t.Subpart.Side, t.Subpart.Front} assert not t.implies(example, intersect_pos) assert not t.implies(intersect_pos, example) assert not t.contradiction(example.union(intersect_pos)) + intersect_neg = {t.Subpart.Side, -t.Subpart.Back} assert not t.implies(example, intersect_neg) assert not t.implies(intersect_neg, example) assert not t.contradiction(example.union(intersect_neg)) + assert not t.implies({t.Subpart.Top, -t.Subpart.Bottom}, {t.Subpart.Top, t.Subpart.Bottom}) + \ No newline at end of file From dbc976819ad2360cf92d4d2d69fccb982f77863d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 064/727] Add 77 lines to tests/constraints/test_reldom.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_reldom.py | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/constraints/test_reldom.py diff --git a/tests/constraints/test_reldom.py b/tests/constraints/test_reldom.py new file mode 100644 index 000000000..202364f52 --- /dev/null +++ b/tests/constraints/test_reldom.py @@ -0,0 +1,77 @@ +# Copyright (c) Princeton University. +# This 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 copy +import pytest + +from pprint import pprint + +from infinigen.core import tags as t + +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r, +) + +def test_reldom_compatible_floorwall(): + + room = r.Domain({t.Semantics.Room, -t.Semantics.Object}, []) + + nofloorrel = ( + -cl.StableAgainst({}, {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Ceiling, -t.Subpart.Wall}), + room + ) + + against = cl.StableAgainst( + {t.Subpart.Back, -t.Subpart.Top, -t.Subpart.Front}, + {t.Subpart.Visible, t.Subpart.Wall, -t.Subpart.SupportSurface, -t.Subpart.Ceiling} + ) + wallrel = (against, room) + + assert r.reldom_compatible(nofloorrel, wallrel) + assert r.reldom_compatible(wallrel, nofloorrel) + +def test_reldom_compatible_negation(): + + nofloorrel = ( + -cl.StableAgainst({}, {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Ceiling, -t.Subpart.Wall}), + r.Domain({t.Semantics.Room, -t.Semantics.Object}, []) + ) + + on = cl.StableAgainst( + {t.Subpart.Bottom, -t.Subpart.Front, -t.Subpart.Top, -t.Subpart.Back}, + {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Wall, -t.Subpart.Ceiling} + ) + specific_floorrel = (on, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + + assert r.reldom_compatible(specific_floorrel, specific_floorrel) + assert not r.reldom_compatible(nofloorrel, specific_floorrel) + assert not r.reldom_compatible(specific_floorrel, nofloorrel) + +def test_reldom_intersects(): + + onroom = ( + cl.StableAgainst( + {t.Subpart.Bottom, -t.Subpart.Front, -t.Subpart.Top, -t.Subpart.Back}, + {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Wall, -t.Subpart.Ceiling} + ), + r.Domain({t.Semantics.Room, -t.Semantics.Object}, []) + ) + + onlivingroom = ( + cl.StableAgainst( + {t.Subpart.Bottom, -t.Subpart.Front, -t.Subpart.Top, -t.Subpart.Back}, + {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Wall, -t.Subpart.Ceiling} + ), + r.Domain({t.Semantics.LivingRoom, t.Semantics.Room, -t.Semantics.Object, -t.Semantics.Bedroom, -t.Semantics.DiningRoom}, []) + ) + + assert r.reldom_intersects(onroom, onlivingroom) + +def test_reldom_negative_contradict(): + + a = (-cl.AnyRelation(), r.Domain({t.Semantics.Object, -t.Semantics.Room}, [])) + assert r.reldom_compatible(a, a) \ No newline at end of file From c7823385717c034f007d04dfc132a6579933e1e5 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 065/727] Add 83 lines to tests/constraints/test_constraint_relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/test_constraint_relations.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/constraints/test_constraint_relations.py diff --git a/tests/constraints/test_constraint_relations.py b/tests/constraints/test_constraint_relations.py new file mode 100644 index 000000000..071b1f6a5 --- /dev/null +++ b/tests/constraints/test_constraint_relations.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 + +from pprint import pprint + +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r +) +from infinigen_examples.indoor_constraint_examples import home_constraints + +def test_relation_implies_trivial(): + + assert sf.implies(sf) + assert sfi.implies(sf) + assert sf.implies(cl.AnyRelation()) + assert sfi.implies(cl.AnyRelation()) + assert not cl.AnyRelation().implies(sf) + assert not cl.AnyRelation().implies(sfi) + + assert a.intersects(b) == truth + assert b.intersects(a) == truth + +example = cl.StableAgainst( + {t.Subpart.Top, -t.Subpart.Bottom}, + {t.Semantics.Object, -t.Subpart.Top} +) + +def test_relations_intersects_unrestricted(): + + unrestricted = cl.AnyRelation() + require_intersects(example, unrestricted, True) + require_intersects(example, -unrestricted, False) + require_intersects(-example, unrestricted, True) + +def test_relation_intersects_mismatched_type(): + mismatch_type = cl.Touching(example.child_tags, example.parent_tags) + require_intersects(example, mismatch_type, False) + require_intersects(example, -mismatch_type, True) + require_intersects(-example, mismatch_type, True) + +def test_relation_intersects_superset(): + require_intersects(example, superset, True) + require_intersects(example, -superset, True) # Top-Bot,Obj-Top AND NOT(Top,Obj) permits Top+Bot_Obj+Top + require_intersects(-example, superset, False) # Top,Obj AND NOT(Top-Bot,Obj-Top) False + +def test_relation_intersects_subset(): + subset = cl.StableAgainst( + ) + require_intersects(example, subset, True) + require_intersects(example, -subset, False) # Top-Bot,Obj-Top AND NOT Top-Bot+Sup,Obj-Top-Side + require_intersects(-example, subset, True) # Top-Bot+Sup,Obj-Top-Side AND NOT Top-Bot,Obj-Top + +def test_relation_intersects_intersecting(): + require_intersects(example, inter, True) + require_intersects(example, -inter, True) # Top-Bot_Obj-Top AND NOT Top+Vis_Obj+Furn. Yes, Top-Bot-Vis_Obj-Top-Furn + require_intersects(-example, inter, True) # Top+Vis_Obj+Furn AND NOT Top-Bot_Obj-Top. + +def test_relation_intersects_contradict_child(): + require_intersects(example, contradict_child, False) + require_intersects(example, -contradict_child, True) + require_intersects(-example, contradict_child, True) # Top+Bot,Obj AND NOT Top-Bot,Obj-Top = Top+Bot,Obj? + +def test_relation_intersects_contradict_parent(): + require_intersects(example, contradict_parent, False) + require_intersects(example, -contradict_parent, True) + require_intersects(-example, contradict_parent, True) + +def test_relation_difference(): + + assert t.difference( + {t.Semantics.Object, -t.Subpart.Top}, + {t.Semantics.Object, t.Subpart.Bottom} + ) == {t.Semantics.Object, -t.Subpart.Top, -t.Subpart.Bottom} + + refine = cl.StableAgainst(set(), {t.Semantics.Object, t.Subpart.Bottom}) + assert example.difference(refine) == cl.StableAgainst( + {t.Subpart.Top, -t.Subpart.Bottom}, + {t.Semantics.Object, -t.Subpart.Top, -t.Subpart.Bottom} + ) From 7f5d57a243bde52535717a7d4c48e5bbd1879817 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 066/727] Add 13 lines to tests/constraints/test_constraint_relations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/constraints/test_constraint_relations.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/constraints/test_constraint_relations.py b/tests/constraints/test_constraint_relations.py index 071b1f6a5..f1ddf37ee 100644 --- a/tests/constraints/test_constraint_relations.py +++ b/tests/constraints/test_constraint_relations.py @@ -10,10 +10,16 @@ constraint_language as cl, reasoning as r ) +from infinigen.core import tags as t from infinigen_examples.indoor_constraint_examples import home_constraints def test_relation_implies_trivial(): + assert not cl.StableAgainst(set(), set()).implies(cl.Touching()) + + sf = cl.SupportedBy({t.Subpart.SupportSurface}) + sfi = cl.SupportedBy({t.Subpart.SupportSurface, t.Subpart.Visible}) + assert sf.implies(sf) assert sfi.implies(sf) assert sf.implies(cl.AnyRelation()) @@ -21,6 +27,7 @@ def test_relation_implies_trivial(): assert not cl.AnyRelation().implies(sf) assert not cl.AnyRelation().implies(sfi) +def require_intersects(a: cl.Relation, b: cl.Relation, truth): assert a.intersects(b) == truth assert b.intersects(a) == truth @@ -43,28 +50,34 @@ def test_relation_intersects_mismatched_type(): require_intersects(-example, mismatch_type, True) def test_relation_intersects_superset(): + superset = cl.StableAgainst({t.Subpart.Top}, {t.Semantics.Object}) require_intersects(example, superset, True) require_intersects(example, -superset, True) # Top-Bot,Obj-Top AND NOT(Top,Obj) permits Top+Bot_Obj+Top require_intersects(-example, superset, False) # Top,Obj AND NOT(Top-Bot,Obj-Top) False def test_relation_intersects_subset(): subset = cl.StableAgainst( + {t.Subpart.Top, -t.Subpart.Bottom, t.Subpart.SupportSurface}, + {t.Semantics.Object, -t.Subpart.Top, -t.Subpart.Side} ) require_intersects(example, subset, True) require_intersects(example, -subset, False) # Top-Bot,Obj-Top AND NOT Top-Bot+Sup,Obj-Top-Side require_intersects(-example, subset, True) # Top-Bot+Sup,Obj-Top-Side AND NOT Top-Bot,Obj-Top def test_relation_intersects_intersecting(): + inter = cl.StableAgainst({t.Subpart.Top, t.Subpart.Visible}, {t.Semantics.Object, t.Semantics.Furniture}) require_intersects(example, inter, True) require_intersects(example, -inter, True) # Top-Bot_Obj-Top AND NOT Top+Vis_Obj+Furn. Yes, Top-Bot-Vis_Obj-Top-Furn require_intersects(-example, inter, True) # Top+Vis_Obj+Furn AND NOT Top-Bot_Obj-Top. def test_relation_intersects_contradict_child(): + contradict_child = cl.StableAgainst({t.Subpart.Top, t.Subpart.Bottom}, {t.Semantics.Object}) require_intersects(example, contradict_child, False) require_intersects(example, -contradict_child, True) require_intersects(-example, contradict_child, True) # Top+Bot,Obj AND NOT Top-Bot,Obj-Top = Top+Bot,Obj? def test_relation_intersects_contradict_parent(): + contradict_parent = cl.StableAgainst({t.Subpart.Top}, {t.Semantics.Object, t.Subpart.Top}) require_intersects(example, contradict_parent, False) require_intersects(example, -contradict_parent, True) require_intersects(-example, contradict_parent, True) From d6b84bb8c24a98efbb525b08e079383caef84b25 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 067/727] Add 170 lines to tests/constraints/test_constraint_domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/constraints/test_constraint_domain.py | 170 ++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/constraints/test_constraint_domain.py diff --git a/tests/constraints/test_constraint_domain.py b/tests/constraints/test_constraint_domain.py new file mode 100644 index 000000000..1da760736 --- /dev/null +++ b/tests/constraints/test_constraint_domain.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: Alexander Raistrick + +import logging + +from infinigen.core.constraints import ( + reasoning as r, + constraint_language as cl +) + +from infinigen_examples.util import constraint_util as cu + +def test_domain_obj(): + + + assert sofas.implies(furniture) + assert not furniture.implies(sofas) + + assert not furniture.implies(furniture_in_livingroom) + assert furniture_in_livingroom.implies(furniture) + + assert not furniture_in_livingroom.implies(furniture_in_bathroom) + assert not furniture_in_bathroom.implies(furniture_in_livingroom) + +def test_domain_implies_complex(): + + against_wall = cl.StableAgainst( + margin=0 + ) + + d = r.Domain( + relations=[ + ( + against_wall, + ) + ] + ) + + assert d.implies(d) + assert not r.Domain(set(), d.relations).implies(d) + assert d.implies(r.Domain(set(), d.relations)) + + # "storage related any way to any thing" is less specific + generalize_relation = r.Domain( + relations=[(cl.AnyRelation(), r.Domain())]) + assert d.implies(generalize_relation) + assert not generalize_relation.implies(d) + + # "storage related any way but specifically to bedroom" + # is both more and less specific so not a subset either way + different_relation = r.Domain( + ) + assert not d.implies(different_relation) + assert not different_relation.implies(d) + + # "storage against a bedroom wall" is more specific + against_bedroom_wall = r.Domain( + ) + assert against_bedroom_wall.implies(d) + assert not d.implies(against_bedroom_wall) + +def test_domain_var_substitute(): + + + assert r.domain_tag_substitute(start, var, subfor) == r.Domain( + relations=[ + ] + ) + + assert r.domain_tag_substitute(start2, var, subfor) == r.Domain( + relations=[ + ] + ) + +def test_domain_intersect_tags(): + + obj = cl.scene()[Semantics.Object].excludes(obj_types) + room = cl.scene()[Semantics.Room].excludes(obj_types) + + ld = r.constraint_domain(obj) + + md = r.constraint_domain(room) + + assert not ld.intersects(md) + assert not ld.intersects(md) + +def test_domain_construction_complex(): + + dom = r.Domain() + dom.add_relation(cl.AnyRelation(), r.Domain({t.Semantics.Object}, [])) + dom.add_relation(cl.StableAgainst(), r.Domain({t.Semantics.Object, t.Variable('room')}, [])) + dom.add_relation(-cl.AnyRelation(), r.Domain({t.Semantics.Room}, [])) + + assert dom.relations[0] == (cl.StableAgainst(), r.Domain({t.Semantics.Object, t.Variable('room')}, [])) + assert dom.relations[1] == (-cl.AnyRelation(), r.Domain({t.Semantics.Room}, [])) + assert len(dom.relations) == 2 + +def test_domain_construction_complex_2(): + + rd1 = (cl.AnyRelation(), r.Domain({t.Semantics.Room, t.Semantics.DiningRoom})) + rd2 = (cl.StableAgainst(), r.Domain({t.Semantics.Room})) + + assert r.reldom_intersects(rd1, rd2) + + dom = r.Domain() + dom.add_relation(*rd1) + dom.add_relation(*rd2) + + print("DOM RESULT", dom) + assert dom.relations == [ + (cl.StableAgainst(), r.Domain({t.Semantics.Room, t.Semantics.DiningRoom})) + ] + +def test_domain_satisfies(): + + a = r.Domain({t.Semantics.Object, -t.Semantics.Room}) + b = r.Domain({t.Semantics.Object, t.Semantics.Room}) + assert not b.satisfies(a) + + b = r.Domain({t.Semantics.Object, -t.Semantics.Room}) + assert b.satisfies(a) + + a.add_relation(cl.StableAgainst(), r.Domain({t.Semantics.Room})) + assert not b.satisfies(a) + + b.add_relation(cl.StableAgainst(), r.Domain({t.Semantics.Room, t.Semantics.DiningRoom})) + assert b.satisfies(a) + + a.add_relation(-cl.AnyRelation(), r.Domain({t.Semantics.Object})) + assert b.satisfies(a) + + b.add_relation(cl.StableAgainst(), r.Domain({t.Semantics.Object})) + assert not b.satisfies(a) + +def test_domain_satisfies_2(): + + res_dom = r.Domain( + {Semantics.Object, Semantics.Storage, -Semantics.Room}, [ + ( + cl.StableAgainst( + {t.Subpart.Bottom, -t.Subpart.Top, -t.Subpart.Back, -t.Subpart.Front}, + {t.Subpart.Visible, t.Subpart.SupportSurface, -t.Subpart.Ceiling, -t.Subpart.Wall} + ), + r.Domain({Semantics.DiningRoom, Semantics.Room, -Semantics.Object}, []) + ), + ( + -cl.AnyRelation(), + r.Domain({Semantics.Object, -Semantics.Room}, []) + ) + ] + ) + + filter_dom = r.Domain( + {Semantics.Object, -Semantics.Room}, + [ + ( + cl.StableAgainst( + {}, + {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Ceiling, -t.Subpart.Wall} + ), + r.Domain({Semantics.DiningRoom, Semantics.Room, -Semantics.Object}, []) + ), + (-cl.AnyRelation(), r.Domain({Semantics.Object, -Semantics.Room}, [])) + ] + ) + + assert res_dom.satisfies(filter_dom) \ No newline at end of file From 5d8124d2dce532211dbff3109aef363445ddf6d5 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 068/727] Add 29 lines to tests/constraints/test_constraint_domain.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/constraints/test_constraint_domain.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/constraints/test_constraint_domain.py b/tests/constraints/test_constraint_domain.py index 1da760736..de24559e4 100644 --- a/tests/constraints/test_constraint_domain.py +++ b/tests/constraints/test_constraint_domain.py @@ -10,41 +10,54 @@ reasoning as r, constraint_language as cl ) +from infinigen.core.constraints.constraint_language import Semantics +from infinigen.core import tags as t from infinigen_examples.util import constraint_util as cu def test_domain_obj(): + furniture = r.Domain({t.Semantics.Furniture}) + sofas = r.Domain({t.Semantics.Furniture, t.Semantics.Seating}) assert sofas.implies(furniture) assert not furniture.implies(sofas) + furniture_in_livingroom = r.Domain({t.Semantics.Furniture}, relations=[(cl.SupportedBy(), r.Domain({t.Semantics.LivingRoom}))]) assert not furniture.implies(furniture_in_livingroom) assert furniture_in_livingroom.implies(furniture) + furniture_in_bathroom = r.Domain({t.Semantics.Furniture}, relations=[(cl.SupportedBy(), r.Domain({t.Semantics.Bathroom}))]) assert not furniture_in_livingroom.implies(furniture_in_bathroom) assert not furniture_in_bathroom.implies(furniture_in_livingroom) def test_domain_implies_complex(): against_wall = cl.StableAgainst( + child_tags={t.Subpart.Back}, + parent_tags={t.Subpart.Wall, t.Subpart.Interior}, margin=0 ) d = r.Domain( + tags={Semantics.Storage}, relations=[ ( against_wall, + r.Domain(tags={Semantics.Room}, relations=[]) ) ] ) assert d.implies(d) + assert not r.Domain({t.Semantics.Storage}).implies(d) assert not r.Domain(set(), d.relations).implies(d) + assert d.implies(r.Domain({t.Semantics.Storage})) assert d.implies(r.Domain(set(), d.relations)) # "storage related any way to any thing" is less specific generalize_relation = r.Domain( + {t.Semantics.Storage}, relations=[(cl.AnyRelation(), r.Domain())]) assert d.implies(generalize_relation) assert not generalize_relation.implies(d) @@ -52,37 +65,53 @@ def test_domain_implies_complex(): # "storage related any way but specifically to bedroom" # is both more and less specific so not a subset either way different_relation = r.Domain( + {t.Semantics.Storage}, + relations=[(cl.AnyRelation(), r.Domain({t.Semantics.Room, t.Semantics.Bedroom}))] ) assert not d.implies(different_relation) assert not different_relation.implies(d) # "storage against a bedroom wall" is more specific against_bedroom_wall = r.Domain( + {t.Semantics.Storage}, + relations=[(against_wall, r.Domain({t.Semantics.Room, t.Semantics.Bedroom}))] ) assert against_bedroom_wall.implies(d) assert not d.implies(against_bedroom_wall) def test_domain_var_substitute(): + var = t.Variable('x') + start = r.Domain({t.Subpart.Interior, var}, relations=[(cl.AnyRelation(), r.Domain())]) + subfor = r.Domain({t.Semantics.Room, t.Semantics.Bedroom}, relations=[(cl.Touching(), r.Domain({t.Semantics.Furniture}))]) assert r.domain_tag_substitute(start, var, subfor) == r.Domain( + {t.Semantics.Room, t.Semantics.Bedroom, t.Subpart.Interior}, relations=[ + (cl.Touching(), r.Domain({t.Semantics.Furniture})) ] ) + start2 = r.Domain({t.Subpart.Interior, var}, relations=[(cl.AnyRelation(), r.Domain({t.Semantics.Lighting}))]) assert r.domain_tag_substitute(start2, var, subfor) == r.Domain( + {t.Semantics.Room, t.Semantics.Bedroom, t.Subpart.Interior}, relations=[ + (cl.AnyRelation(), r.Domain({t.Semantics.Lighting})), # not implied so gets kept + (cl.Touching(), r.Domain({t.Semantics.Furniture})) ] ) def test_domain_intersect_tags(): + obj_types = {Semantics.Object, Semantics.Room, Semantics.Cutter} obj = cl.scene()[Semantics.Object].excludes(obj_types) room = cl.scene()[Semantics.Room].excludes(obj_types) ld = r.constraint_domain(obj) + assert -Semantics.Room in ld.tags md = r.constraint_domain(room) + assert -Semantics.Object in md.tags assert not ld.intersects(md) assert not ld.intersects(md) From e9026d02f6e9c6c3b6e21843ea04bd449bd00e9c Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 069/727] Add 84 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- tests/solver/test_stable_against.py | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/solver/test_stable_against.py diff --git a/tests/solver/test_stable_against.py b/tests/solver/test_stable_against.py new file mode 100644 index 000000000..757aa608a --- /dev/null +++ b/tests/solver/test_stable_against.py @@ -0,0 +1,84 @@ +# Copyright (c) Princeton University. +# This 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 + """Create a scene with a table and a cup, and return the state.""" + # butil.save_blend('test.blend') + +def test_horizontal_stability(): + butil.clear_scene() + objs = {} + + table = butil.spawn_cube(name='table') + table.dimensions = (4,10,2) + + chair1 = butil.spawn_cube(name='chair1') + chair1.dimensions = (2,2,3) + chair1.location = (3,3,0) + + chair2 = butil.spawn_cube(name='chair2') + chair2.dimensions = (2,2,3) + chair2.location = (3,-3,0) + + chair3 = butil.spawn_cube(name='chair3') + chair3.dimensions = (2,2,3) + chair3.location = (-3,3,0) + + chair4 = butil.spawn_cube(name='chair4') + chair4.dimensions = (2,2,3) + chair4.location = (-3,-3,0) + for o in [table, chair1, chair2, chair3, chair4]: + butil.apply_transform(o) + parse_scene.preprocess_obj(o) + tagging.tag_canonical_surfaces(o) + with butil.SelectObjects([table, chair1, chair2, chair3, chair4]): + # rotate + bpy.ops.transform.rotate(value=np.pi/4, orient_axis='Z', orient_type='GLOBAL') + # butil.save_blend('test.blend') + bpy.context.view_layer.update() + + + + objs['table'] = state_def.ObjectState(table) + objs['chair1'] = state_def.ObjectState(chair1) + objs['chair2'] = state_def.ObjectState(chair2) + objs['chair3'] = state_def.ObjectState(chair3) + objs['chair4'] = state_def.ObjectState(chair4) + objs['chair1'].relations.append( + state_def.RelationState( + target_name='table', + child_plane_idx=0, + parent_plane_idx=0 + ) + ) + objs['chair2'].relations.append( + state_def.RelationState( + target_name='table', + child_plane_idx=0, + parent_plane_idx=0 + ) + ) + objs['chair3'].relations.append( + state_def.RelationState( + target_name='table', + child_plane_idx=0, + parent_plane_idx=0 + ) + ) + objs['chair4'].relations.append( + state_def.RelationState( + target_name='table', + child_plane_idx=0, + parent_plane_idx=0 + ) + ) + state = state_def.State(objs=objs) + assert validity.check_post_move_validity(state, 'chair1') + assert validity.check_post_move_validity(state, 'chair2') + assert validity.check_post_move_validity(state, 'chair3') + assert validity.check_post_move_validity(state, 'chair4') + + # butil.save_blend('test.blend') + +if __name__ == '__main__': + test_horizontal_stability() \ No newline at end of file From 22cff9e789b0781df9740410e51f253f69dd2a1b Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:11 -0700 Subject: [PATCH 070/727] Add 74 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_stable_against.py | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/solver/test_stable_against.py b/tests/solver/test_stable_against.py index 757aa608a..191caae87 100644 --- a/tests/solver/test_stable_against.py +++ b/tests/solver/test_stable_against.py @@ -2,9 +2,81 @@ # This 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 +from itertools import chain +from functools import partial + +# import pytest +import numpy as np +import sys +import os + +from infinigen.core.constraints.example_solver.geometry import dof, parse_scene, planes, stability, validity +from mathutils import Vector + +from infinigen.core.constraints import ( + usage_lookup, + example_solver as solver, + constraint_language as cl +) +from infinigen.core.util import blender as butil +from infinigen.core.constraints.example_solver import ( + state_def +) + +def make_scene(loc2): """Create a scene with a table and a cup, and return the state.""" + butil.clear_scene() + objs = {} + + table = butil.spawn_cube(scale=(5, 5, 1), name='table') + cup = butil.spawn_cube(scale=(1, 1, 1), name='cup', location=loc2) + + for o in [table, cup]: + butil.apply_transform(o) + parse_scene.preprocess_obj(o) + tagging.tag_canonical_surfaces(o) + + assert table.scale == Vector((1,1,1)) + assert cup.location != Vector((0,0,0)) + + bpy.context.view_layer.update() + + objs['table'] = state_def.ObjectState(table) + objs['cup'] = state_def.ObjectState(cup) + objs['cup'].relations.append( + state_def.RelationState( + target_name='table', + child_plane_idx=0, + parent_plane_idx=0 + ) + ) + # butil.save_blend('test.blend') + return state_def.State(objs=objs) + +def test_stable_against(): + + # too low, intersects ground + assert not validity.check_post_move_validity(make_scene((0, 0, 0.5)), 'cup') + + # exactly touches surface + assert validity.check_post_move_validity(make_scene((0, 0, 1)), 'cup') + + # underneath + assert not validity.check_post_move_validity(make_scene((0, 0, -3)), 'cup') + + # exactly at corner + assert validity.check_post_move_validity(make_scene((2, 2, 1)), 'cup') + + # slightly over corner + assert not validity.check_post_move_validity(make_scene((2.1, 2.1, 1)), 'cup') + + # farr away + assert not validity.check_post_move_validity(make_scene((4, 4, 0.5)), 'cup') + def test_horizontal_stability(): butil.clear_scene() objs = {} @@ -80,5 +152,7 @@ def test_horizontal_stability(): # butil.save_blend('test.blend') + + if __name__ == '__main__': test_horizontal_stability() \ No newline at end of file From 54bde8336651d026699ced22d438f0b34420bb53 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 071/727] Add 6 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/solver/test_stable_against.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/solver/test_stable_against.py b/tests/solver/test_stable_against.py index 191caae87..005574260 100644 --- a/tests/solver/test_stable_against.py +++ b/tests/solver/test_stable_against.py @@ -20,6 +20,7 @@ example_solver as solver, constraint_language as cl ) +from infinigen.core import tagging, tags as t from infinigen.core.util import blender as butil from infinigen.core.constraints.example_solver import ( state_def @@ -47,6 +48,7 @@ def make_scene(loc2): objs['cup'] = state_def.ObjectState(cup) objs['cup'].relations.append( state_def.RelationState( + cl.StableAgainst({t.Subpart.Bottom}, {t.Subpart.Top}), target_name='table', child_plane_idx=0, parent_plane_idx=0 @@ -118,6 +120,7 @@ def test_horizontal_stability(): objs['chair4'] = state_def.ObjectState(chair4) objs['chair1'].relations.append( state_def.RelationState( + cl.StableAgainst({t.Subpart.Back}, {t.Subpart.Front}, check_z=False), target_name='table', child_plane_idx=0, parent_plane_idx=0 @@ -125,6 +128,7 @@ def test_horizontal_stability(): ) objs['chair2'].relations.append( state_def.RelationState( + cl.StableAgainst({t.Subpart.Back}, {t.Subpart.Front}, check_z=False), target_name='table', child_plane_idx=0, parent_plane_idx=0 @@ -132,6 +136,7 @@ def test_horizontal_stability(): ) objs['chair3'].relations.append( state_def.RelationState( + cl.StableAgainst({t.Subpart.Front}, {t.Subpart.Back}, check_z=False), target_name='table', child_plane_idx=0, parent_plane_idx=0 @@ -139,6 +144,7 @@ def test_horizontal_stability(): ) objs['chair4'].relations.append( state_def.RelationState( + cl.StableAgainst({t.Subpart.Front}, {t.Subpart.Back}, check_z=False), target_name='table', child_plane_idx=0, parent_plane_idx=0 From 1264ae3e49ad6b0876c26d787b947d9e9351a1ea Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 072/727] Add 679 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- tests/solver/test_constraint_evaluator.py | 679 ++++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 tests/solver/test_constraint_evaluator.py diff --git a/tests/solver/test_constraint_evaluator.py b/tests/solver/test_constraint_evaluator.py new file mode 100644 index 000000000..4574b4923 --- /dev/null +++ b/tests/solver/test_constraint_evaluator.py @@ -0,0 +1,679 @@ +# Copyright (c) Princeton University. +# This 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 itertools import chain +from functools import partial + +from infinigen.core.constraints import ( + usage_lookup, + example_solver as solver, + constraint_language as cl +) +from infinigen.core.constraints.example_solver.state_def import state_from_dummy_scene, State, ObjectState +from infinigen.core.constraints.example_solver.propose_discrete import lookup_generator + +from infinigen.assets.tables.dining_table import TableDiningFactory +from infinigen.assets.seating.chairs import ChairFactory + + butil.clear_scene() + + + butil.clear_scene() + butil.clear_scene() + butil.clear_scene() + constraints = [] + score_terms = [] + + scene = cl.scene() + problem = cl.Problem(constraints, score_terms) + score_terms = [] + problem = cl.Problem(constraints, score_terms) + + + score_terms = [] + problem = cl.Problem(constraints, score_terms) + + +def test_accessibility_monotonicity(): + butil.clear_scene() + scores = [] + butil.clear_scene() + obj_states = {} + col = butil.get_collection("indoor_scene_test") + chairs = butil.get_collection("chair") + tables = butil.get_collection("table") + col.children.link(chairs) + col.children.link(tables) + + chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + butil.put_in_collection(chair, chairs) + + table = butil.spawn_cube(size=2, location=(2+dist, 0, 0), name='table1') + butil.put_in_collection(table, tables) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + print("nonaccessibility scores", scores) + +def test_accessibility_side(): + butil.clear_scene() + obj_states = {} + col = butil.get_collection("indoor_scene_test") + chairs = butil.get_collection("chair") + tables = butil.get_collection("table") + col.children.link(chairs) + col.children.link(tables) + + chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + butil.put_in_collection(chair, chairs) + + table = butil.spawn_cube(size=2, location=(0, 2, 0), name='table1') + butil.put_in_collection(table, tables) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.accessibility_cost(chair, table)] + problem = cl.Problem(constraints, score_terms) + print("nonaccessibility scores", res) + assert np.isclose(res, 0) + +def test_accessibility_angle(): + butil.clear_scene() + scores = [] + for angle in [0, np.pi/4, np.pi/2, np.pi]: + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + + table = butil.spawn_sphere(radius = 1, location=(4*np.cos(angle), 4*np.sin(angle), 0), name='table1') + print(table.location) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.accessibility_cost(chair, table)] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + print("nonaccessibility scores", scores) + assert scores == sorted(scores, reverse=True) + +def test_accessibility_volume(): + butil.clear_scene() + scores = [] + for volume in [1, 2, 3, 4]: + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + table = butil.spawn_sphere(radius=volume, location=(6, 0, 0), name='table1') + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.accessibility_cost(chair, table)] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + print("nonaccessibility scores", scores) + assert scores == sorted(scores) + +# def test_accessibility_speed(): +# scores = [] +# butil.clear_scene() +# obj_states = {} + +# chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') +# blocking_spheres = [butil.spawn_sphere(radius=1, location=(3+i, np.random.rand(), 0), name=f'sphere{i}') for i in range(100)] + +# for s in blocking_spheres: + +# state = State(objs=obj_states) + +# tagging.tag_canonical_surfaces(chair) +# for s in blocking_spheres: +# tagging.tag_canonical_surfaces(s) + +# constraints = [] +# score_terms = [] +# scene = cl.scene() + +# score_terms += [cl.accessibility_cost(chair, table)] +# problem = cl.Problem(constraints, score_terms) +# s = time() +# print(time() - s) +# scores.append(res) +# print("nonaccessibility scores", scores) +# assert scores == sorted(scores) + + +def test_angle_alignment(): + butil.clear_scene() + scores = [] + for angle in np.linspace(0, np.pi/2, 5): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name='chair1') + table = butil.spawn_sphere(radius = 1, location=(0,0, 0), name='table1') + # rotate chair by angle in z direction + chair.rotation_euler = (0, 0, angle) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.angle_alignment_cost(chair, table)] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + # state.trimesh_scene.show() + print("angle_alignment costs", scores) + assert scores == sorted(scores) + +def test_angle_alignment_multiple_objects(): + butil.clear_scene() + scores = [] + + for angle in np.linspace(0, np.pi/2, 5): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name='chair1') + table1 = butil.spawn_sphere(radius=1, location=(0, 0, 0), name='table1') + table2 = butil.spawn_sphere(radius=1, location=(3, 0, 0), name='table2') + + # Rotate chair by angle in z direction + chair.rotation_euler = (0, 0, angle) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table1) + tagging.tag_canonical_surfaces(table2) + + constraints = [] + score_terms = [] + scene = cl.scene() + + + score_terms += [cl.angle_alignment_cost(chair, tables)] + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + print("angle_alignment costs (multiple objects):", scores) + assert scores == sorted(scores) + +def test_angle_alignment_multiple_objects_varying_positions(): + butil.clear_scene() + scores = [] + + for i in range(5): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name='chair') + + table_positions = [ + (0, 0, 0), + (3, 0, 0), + (0, 3, 0), + (-3, 2, 0), + (3, 3, 0) + ] + + tables = [] + for j, pos in enumerate(table_positions[:i+1], start=1): + table = butil.spawn_sphere(radius=1, location=pos, name=f'table{j}') + tables.append(table) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + for table in tables: + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + + score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + print("angle_alignment costs (multiple objects, varying positions):", scores) + assert scores == sorted(scores) + +def test_angle_alignment_multipolygon_projection(): + butil.clear_scene() + scores = [] + + for i in range(5): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name='chair') + + # Create a complex object that may result in a multipolygon projection + table_verts = [ + (-1, -1, 0), + (1, -1, 0), + (1, 1, 0), + (-1, 1, 0), + (0, 0, 1) + ] + table_faces = [ + (0, 1, 2, 3), + (0, 1, 4), + (1, 2, 4), + (2, 3, 4), + (3, 0, 4) + ] + + table_mesh = bpy.data.meshes.new(name="TableMesh") + table_obj = bpy.data.objects.new(name="Table", object_data=table_mesh) + + scene = bpy.context.scene + scene.collection.objects.link(table_obj) + + table_mesh.from_pydata(table_verts, [], table_faces) + table_mesh.update() + + table_obj.location = (0, 0, 0) + + # Rotate the table object based on the iteration + chair.rotation_euler = (0, 0, i*np.pi/10) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table_obj) + + constraints = [] + score_terms = [] + scene = cl.scene() + + + score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + print("angle_alignment costs (multipolygon projection):", scores) + assert sorted(scores) == scores + +def test_angle_alignment_tagged(): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=2, location=(5, 0, 0), name='chair1') + table = butil.spawn_cube(size=2, location=(0,0, 0), name='table1') + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + table.rotation_euler[2] = np.pi/2 + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + + assert np.isclose(res, 0.5, atol=1e-3) + + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=2, location=(5, 0, 0), name='chair1') + table = butil.spawn_cube(size=2, location=(0,0, 0), name='table1') + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + + assert np.isclose(res, 0, atol=1e-3) + + +def test_focus_score(): + butil.clear_scene() + scores = [] + for angle in np.linspace(0, np.pi/2, 5): + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name='chair1') + table = butil.spawn_sphere(radius = 1, location=(0,0, 0), name='table1') + # rotate chair by angle in z direction + chair.rotation_euler = (0, 0, angle) + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.focus_score(chair, table)] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + # state.trimesh_scene.show() + print("focus_score costs", scores) + assert scores == sorted(scores) + + butil.clear_scene() + + butil.clear_scene() + obj_states = {} + + chair = butil.spawn_cube(size=2, location=(0, 0, 2), name='chair1') + table = butil.spawn_cube(size=10, location=(0,0, 0), name='table1') + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair) + tagging.tag_canonical_surfaces(table) + + constraints = [] + score_terms = [] + scene = cl.scene() + + + + problem = cl.Problem(constraints, score_terms) + assert np.isclose(res, 4) + constraints = [] + score_terms = [] + scene = cl.scene() + + # butil.save_blend('table_chair.blend') + problem = cl.Problem(constraints, score_terms) + assert np.isclose(res, 2) + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + assert np.isclose(res, 6) + +def test_table(): + butil.clear_scene() + + used_as = home_asset_usage() + usage_lookup.initialize_from_dict(used_as) + + butil.clear_scene() + gen = TableDiningFactory(0) + + # if fac in pholder_facs: + # obj = fac(0).spawn_placeholder(0, loc=(0,0,0), rot=(0,0,0)) + # elif fac in asset_facs: + # obj = fac(0).spawn_asset(0, loc=(0,0,0), rot=(0,0,0)) + # else: + # raise ValueError() + + 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') + + tagging.tag_canonical_surfaces(obj) + # butil.save_blend('table.blend') + + + # if mask.sum() == 0: + # raise ValueError( + # ) + +def test_reflection_asymmetry(): + + """ + create a bunch of chairs and a table. The chairs are reflected along the long part of the table. + check that the asymmetry score is 0 + """ + scores = [] + butil.clear_scene() + obj_states = {} + + + chairs = [] + for i in range(4): + chairs.append(chair) + # tagging.tag_canonical_surfaces(chair) + chairs[0].location = (1, 1, 0) + chairs[1].location = (1, -1, 0) + chairs[2].location = (-1, 1, 0) + chairs[3].location = (-1, -1, 0) + + chairs[0].rotation_euler = (0, 0, -np.pi/2) + chairs[1].rotation_euler = (0, 0, np.pi/2) + chairs[2].rotation_euler = (0, 0, -np.pi/2) + chairs[3].rotation_euler = (0, 0, np.pi/2) + + bpy.context.view_layer.update() + # chairs[0].rotation_euler = (0, 0, np.pi) + + # butil.save_blend('table_chairs.blend') + + state = State(objs=obj_states) + + # tagging.tag_canonical_surfaces(table) + # for i in range(5): + # tagging.tag_canonical_surfaces(obj_states[f'chair{i}'].obj) + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + print(res) + assert np.isclose(res, 0, atol=1e-2) + + # assert the asymmetry increases as we gradually move one chair away from the table + chairs[0].location = (1, 2, 0) + bpy.context.view_layer.update() + score_terms = [] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + #assert the asymmetry increases if we rotate chair 0 + chairs[0].rotation_euler = (0, 0, 0) + bpy.context.view_layer.update() + score_terms = [] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + print("asymmetry scores", scores) + #assert monotonocity + assert scores == sorted(scores) + # assert it is strict + assert (scores[0] < scores[1]) and scores[1] < scores[2] + + +def test_rotation_asymmetry(): + """ + create a bunch of chairs. The chairs are rotationally symmetric and then perturbed. + """ + scores = [] + butil.clear_scene() + obj_states = {} + + chairs = [] + for i in range(6): + chairs.append(chair) + + circle_locations_rotations = [((2*np.cos(i*np.pi/3), 2*np.sin(i*np.pi/3), 0),i*np.pi/3) for i in range(6)] + np.random.shuffle(circle_locations_rotations) + # put the chairs in a circle + for i in range(6): + chairs[i].location = circle_locations_rotations[i][0] + chairs[i].rotation_euler = (0, 0, circle_locations_rotations[i][1]) + + + + bpy.context.view_layer.update() + + state = State(objs=obj_states) + + + constraints = [] + score_terms = [] + scene = cl.scene() + + problem = cl.Problem(constraints, score_terms) + scores.append(res) + assert np.isclose(res,0, atol=1e-2) + + # assert the asymmetry increases as we gradually move one chair from the circle + chairs[0].location += Vector(np.random.rand(3)) + bpy.context.view_layer.update() + score_terms = [] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + #assert the asymmetry increases if we rotate chair 0 + bpy.context.view_layer.update() + score_terms = [] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + # do the same for another chair + chairs[1].location += Vector(np.random.rand(3)) + bpy.context.view_layer.update() + score_terms = [] + problem = cl.Problem(constraints, score_terms) + scores.append(res) + + # assert monotonic + # assert it is strict + assert (scores[0] < scores[1]) and scores[1] < scores[2] and scores[2] < scores[3] + print("asymmetry scores", scores) + + +def test_coplanarity(): + butil.clear_scene() + obj_states = {} + + chair1 = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + chair2 = butil.spawn_cube(size=2, location=(4, 0, 0), name='chair2') + chair3 = butil.spawn_cube(size=2, location=(8, 0, 0), name='chair3') + chair4 = butil.spawn_cube(size=2, location=(12, 0, 0), name='chair4') + + + state = State(objs=obj_states) + + tagging.tag_canonical_surfaces(chair1) + tagging.tag_canonical_surfaces(chair2) + tagging.tag_canonical_surfaces(chair3) + tagging.tag_canonical_surfaces(chair4) + + constraints = [] + score_terms = [] + scene = cl.scene() + + score_terms += [cl.coplanarity_cost(chairs)] + problem = cl.Problem(constraints, score_terms) + # print(res1) + assert np.isclose(res1, 0, atol=1e-2) + + chair2.location = (4, 2, 0) + bpy.context.view_layer.update() + # butil.save_blend('test.blend') + + state = State(objs=obj_states) + score_terms = [] + score_terms += [cl.coplanarity_cost(chairs)] + problem = cl.Problem(constraints, score_terms) + # print(res2) + assert res2 > res1 + + chair3.rotation_euler = (0, 0, np.pi/6) + bpy.context.view_layer.update() + state = State(objs=obj_states) + score_terms = [] + score_terms += [cl.coplanarity_cost(chairs)] + problem = cl.Problem(constraints, score_terms) + assert res3 > res2 + + chair2.dimensions = (2, 2, 4) + bpy.context.view_layer.update() + state = State(objs=obj_states) + score_terms = [] + score_terms += [cl.coplanarity_cost(chairs)] + problem = cl.Problem(constraints, score_terms) + assert res4 > res3 + + + + + + + + + +if __name__ == '__main__': + # test_min_dist() + # test_reflection_asymmetry() + # test_accessibility_speed() + # test_coplanarity() + test_angle_alignment_multipolygon_projection() From d8c9ab741cbd1fb987c57de43222c592ec607974 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 073/727] Add 218 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_constraint_evaluator.py | 218 ++++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/tests/solver/test_constraint_evaluator.py b/tests/solver/test_constraint_evaluator.py index 4574b4923..17da8e9fa 100644 --- a/tests/solver/test_constraint_evaluator.py +++ b/tests/solver/test_constraint_evaluator.py @@ -4,40 +4,82 @@ # Authors: Karhan Kayan from itertools import chain from functools import partial +from time import time +import sys +import os + +import bpy +import pytest +import numpy as np +from mathutils import Vector from infinigen.core.constraints import ( usage_lookup, example_solver as solver, constraint_language as cl ) +from infinigen.core.constraints.evaluator.node_impl import node_impls from infinigen.core.constraints.example_solver.state_def import state_from_dummy_scene, State, ObjectState +from infinigen.core.util import blender as butil from infinigen.core.constraints.example_solver.propose_discrete import lookup_generator +from infinigen.core import tagging, tags as t +import infinigen.core.constraints.example_solver.moves.addition as addition from infinigen.assets.tables.dining_table import TableDiningFactory from infinigen.assets.seating.chairs import ChairFactory +from infinigen.assets.utils.bbox_from_mesh import bbox_mesh_from_hipoly + +from infinigen_examples.indoor_asset_semantics import home_asset_usage +from infinigen_examples.indoor_constraint_examples import home_constraints +def test_home_constraints_implemented(): butil.clear_scene() + cons = home_constraints() + + for node in cons.traverse(): + continue + assert node.__class__ in node_impls + +def make_chair_table(): + + return col butil.clear_scene() butil.clear_scene() +def test_min_dist(): butil.clear_scene() + + col = make_chair_table() + constraints = [] score_terms = [] scene = cl.scene() + + score_terms += [cl.distance(chair, table)] problem = cl.Problem(constraints, score_terms) + assert np.isclose(evaluate.evaluate_problem(problem, state).loss(), 1) + score_terms = [] + score_terms += [cl.distance(table, chair) + 1] problem = cl.Problem(constraints, score_terms) + assert np.isclose(evaluate.evaluate_problem(problem, state).loss(), 2) + sofas = butil.get_collection("seating") score_terms = [] + score_terms += [cl.distance(chair, sofa) + cl.distance(chair, table)] problem = cl.Problem(constraints, score_terms) + state = state_from_dummy_scene(col) + assert np.isclose(evaluate.evaluate_problem(problem, state).loss(), 3) + butil.clear_scene() def test_accessibility_monotonicity(): butil.clear_scene() scores = [] + for dist in [1,1.5,2,2.5]: butil.clear_scene() obj_states = {} col = butil.get_collection("indoor_scene_test") @@ -47,11 +89,14 @@ def test_accessibility_monotonicity(): col.children.link(tables) chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + chair.rotation_euler = (0, 0, 0.1) butil.put_in_collection(chair, chairs) table = butil.spawn_cube(size=2, location=(2+dist, 0, 0), name='table1') butil.put_in_collection(table, tables) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -62,10 +107,15 @@ def test_accessibility_monotonicity(): score_terms = [] scene = cl.scene() + score_terms += [cl.accessibility_cost(chair, table, dist=3)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) + print("nonaccessibility scores", scores) + assert np.all(np.diff(scores) < 0) + def test_accessibility_side(): butil.clear_scene() obj_states = {} @@ -81,6 +131,8 @@ def test_accessibility_side(): table = butil.spawn_cube(size=2, location=(0, 2, 0), name='table1') butil.put_in_collection(table, tables) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -93,6 +145,7 @@ def test_accessibility_side(): score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() print("nonaccessibility scores", res) assert np.isclose(res, 0) @@ -108,6 +161,8 @@ def test_accessibility_angle(): table = butil.spawn_sphere(radius = 1, location=(4*np.cos(angle), 4*np.sin(angle), 0), name='table1') print(table.location) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -120,6 +175,7 @@ def test_accessibility_angle(): score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("nonaccessibility scores", scores) assert scores == sorted(scores, reverse=True) @@ -134,6 +190,8 @@ def test_accessibility_volume(): chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') table = butil.spawn_sphere(radius=volume, location=(6, 0, 0), name='table1') + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -146,6 +204,7 @@ def test_accessibility_volume(): score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("nonaccessibility scores", scores) assert scores == sorted(scores) @@ -158,7 +217,9 @@ def test_accessibility_volume(): # chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') # blocking_spheres = [butil.spawn_sphere(radius=1, location=(3+i, np.random.rand(), 0), name=f'sphere{i}') for i in range(100)] +# obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) # for s in blocking_spheres: +# obj_states[s.name] = ObjectState(s, tags= {t.Semantics.Table}) # state = State(objs=obj_states) @@ -173,6 +234,7 @@ def test_accessibility_volume(): # score_terms += [cl.accessibility_cost(chair, table)] # problem = cl.Problem(constraints, score_terms) # s = time() +# res = evaluate.evaluate_problem(problem, state).score() # print(time() - s) # scores.append(res) # print("nonaccessibility scores", scores) @@ -191,6 +253,8 @@ def test_angle_alignment(): # rotate chair by angle in z direction chair.rotation_euler = (0, 0, angle) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -203,6 +267,7 @@ def test_angle_alignment(): score_terms += [cl.angle_alignment_cost(chair, table)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) # state.trimesh_scene.show() print("angle_alignment costs", scores) @@ -223,6 +288,9 @@ def test_angle_alignment_multiple_objects(): # Rotate chair by angle in z direction chair.rotation_euler = (0, 0, angle) + obj_states[chair.name] = ObjectState(chair, tags={t.Semantics.Chair}) + obj_states[table1.name] = ObjectState(table1, tags={t.Semantics.Table}) + obj_states[table2.name] = ObjectState(table2, tags={t.Semantics.Table}) state = State(objs=obj_states) @@ -238,6 +306,7 @@ def test_angle_alignment_multiple_objects(): score_terms += [cl.angle_alignment_cost(chair, tables)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("angle_alignment costs (multiple objects):", scores) @@ -265,7 +334,9 @@ def test_angle_alignment_multiple_objects_varying_positions(): for j, pos in enumerate(table_positions[:i+1], start=1): table = butil.spawn_sphere(radius=1, location=pos, name=f'table{j}') tables.append(table) + obj_states[table.name] = ObjectState(table, tags={t.Semantics.Table}) + obj_states[chair.name] = ObjectState(chair, tags={t.Semantics.Chair}) state = State(objs=obj_states) @@ -281,6 +352,7 @@ def test_angle_alignment_multiple_objects_varying_positions(): score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("angle_alignment costs (multiple objects, varying positions):", scores) @@ -326,6 +398,8 @@ def test_angle_alignment_multipolygon_projection(): # Rotate the table object based on the iteration chair.rotation_euler = (0, 0, i*np.pi/10) + obj_states[chair.name] = ObjectState(chair, tags={t.Semantics.Chair}) + obj_states[table_obj.name] = ObjectState(table_obj, tags={t.Semantics.Table}) state = State(objs=obj_states) @@ -340,6 +414,7 @@ def test_angle_alignment_multipolygon_projection(): score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("angle_alignment costs (multipolygon projection):", scores) @@ -352,6 +427,8 @@ def test_angle_alignment_tagged(): chair = butil.spawn_cube(size=2, location=(5, 0, 0), name='chair1') table = butil.spawn_cube(size=2, location=(0,0, 0), name='table1') + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -365,6 +442,7 @@ def test_angle_alignment_tagged(): scene = cl.scene() problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 0.5, atol=1e-3) @@ -374,6 +452,8 @@ def test_angle_alignment_tagged(): chair = butil.spawn_cube(size=2, location=(5, 0, 0), name='chair1') table = butil.spawn_cube(size=2, location=(0,0, 0), name='table1') + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -385,6 +465,7 @@ def test_angle_alignment_tagged(): scene = cl.scene() problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 0, atol=1e-3) @@ -401,6 +482,8 @@ def test_focus_score(): # rotate chair by angle in z direction chair.rotation_euler = (0, 0, angle) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -413,19 +496,59 @@ def test_focus_score(): score_terms += [cl.focus_score(chair, table)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) # state.trimesh_scene.show() print("focus_score costs", scores) assert scores == sorted(scores) +@pytest.mark.skip +def test_viol_amounts(): butil.clear_scene() + def mk_state(n): + + butil.clear_scene() + obj_states = {} + + for i in range(n): + chair = butil.spawn_cube(size=1, location=(-3, 0, 0), name=f'chair{i}') + obj_states[chair.name] = ObjectState(chair, tags={t.Semantics.Furniture, t.Semantics.Chair}) + + return State(objs=obj_states) + + assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 1 + assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(7))[1] == 4 + + assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(4))[1] == 1 + assert evaluate.evaluate_problem(cons, mk_state(6))[1] == 3 + + assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 3 + assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 2 + assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(4))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(6))[1] == 0 + + assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 3 + assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 2 + assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 + assert evaluate.evaluate_problem(cons, mk_state(4))[1] == 1 + assert evaluate.evaluate_problem(cons, mk_state(6))[1] == 3 + +def test_min_dist_tagged(): + butil.clear_scene() obj_states = {} chair = butil.spawn_cube(size=2, location=(0, 0, 2), name='chair1') table = butil.spawn_cube(size=10, location=(0,0, 0), name='table1') + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) state = State(objs=obj_states) @@ -439,13 +562,16 @@ def test_focus_score(): problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 4) + constraints = [] score_terms = [] scene = cl.scene() # butil.save_blend('table_chair.blend') problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 2) constraints = [] @@ -453,8 +579,10 @@ def test_focus_score(): scene = cl.scene() problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 6) + def test_table(): butil.clear_scene() @@ -463,6 +591,7 @@ def test_table(): butil.clear_scene() gen = TableDiningFactory(0) + obj = bbox_mesh_from_hipoly(gen, 0, use_pholder=False) # if fac in pholder_facs: # obj = fac(0).spawn_placeholder(0, loc=(0,0,0), rot=(0,0,0)) @@ -479,9 +608,14 @@ def test_table(): tagging.tag_canonical_surfaces(obj) # butil.save_blend('table.blend') + # obj_tags = tagging.union_object_tags(obj) + # for tag in [t.Semantics.Back, t.Semantics.Bottom, t.Semantics.SupportSurface]: + # mask = tagging.tagged_face_mask(obj, {tag}) # if mask.sum() == 0: + # obj_tags = tagging.union_object_tags(obj) # raise ValueError( + # f'{obj.name=} has nothing tagged for {tag=}. {obj_tags=}' # ) def test_reflection_asymmetry(): @@ -494,11 +628,16 @@ def test_reflection_asymmetry(): butil.clear_scene() obj_states = {} + table = bbox_mesh_from_hipoly(TableDiningFactory(0), 0, use_pholder=False) + obj_states[table.name] = ObjectState(table, tags= {t.Semantics.Table}) chairs = [] for i in range(4): + chair = bbox_mesh_from_hipoly(ChairFactory(0), i, use_pholder=False) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) chairs.append(chair) # tagging.tag_canonical_surfaces(chair) + # tagging.extract_tagged_faces(chair, {t.Semantics.Front}) chairs[0].location = (1, 1, 0) chairs[1].location = (1, -1, 0) chairs[2].location = (-1, 1, 0) @@ -523,8 +662,12 @@ def test_reflection_asymmetry(): constraints = [] score_terms = [] scene = cl.scene() + table_tagged = cl.tagged(scene, {t.Semantics.Table}) + chairs_tagged = cl.tagged(scene, {t.Semantics.Chair}) + score_terms += [cl.reflectional_asymmetry(chairs_tagged, table_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print(res) assert np.isclose(res, 0, atol=1e-2) @@ -533,14 +676,18 @@ def test_reflection_asymmetry(): chairs[0].location = (1, 2, 0) bpy.context.view_layer.update() score_terms = [] + score_terms += [cl.reflectional_asymmetry(chairs_tagged, table_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) #assert the asymmetry increases if we rotate chair 0 chairs[0].rotation_euler = (0, 0, 0) bpy.context.view_layer.update() score_terms = [] + score_terms += [cl.reflectional_asymmetry(chairs_tagged, table_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) print("asymmetry scores", scores) @@ -550,6 +697,7 @@ def test_reflection_asymmetry(): assert (scores[0] < scores[1]) and scores[1] < scores[2] +@pytest.mark.skip def test_rotation_asymmetry(): """ create a bunch of chairs. The chairs are rotationally symmetric and then perturbed. @@ -560,6 +708,8 @@ def test_rotation_asymmetry(): chairs = [] for i in range(6): + chair = bbox_mesh_from_hipoly(ChairFactory(0), i, use_pholder=False) + obj_states[chair.name] = ObjectState(chair, tags= {t.Semantics.Chair}) chairs.append(chair) circle_locations_rotations = [((2*np.cos(i*np.pi/3), 2*np.sin(i*np.pi/3), 0),i*np.pi/3) for i in range(6)] @@ -579,8 +729,11 @@ def test_rotation_asymmetry(): constraints = [] score_terms = [] scene = cl.scene() + chairs_tagged = cl.tagged(scene, {t.Semantics.Chair}) + score_terms += [cl.rotational_asymmetry(chairs_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) assert np.isclose(res,0, atol=1e-2) @@ -588,23 +741,32 @@ def test_rotation_asymmetry(): chairs[0].location += Vector(np.random.rand(3)) bpy.context.view_layer.update() score_terms = [] + score_terms += [cl.rotational_asymmetry(chairs_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) #assert the asymmetry increases if we rotate chair 0 + chairs[0].rotation_euler = (0, 0, np.random.rand(1)) bpy.context.view_layer.update() score_terms = [] + score_terms += [cl.rotational_asymmetry(chairs_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) # do the same for another chair chairs[1].location += Vector(np.random.rand(3)) bpy.context.view_layer.update() score_terms = [] + score_terms += [cl.rotational_asymmetry(chairs_tagged)] problem = cl.Problem(constraints, score_terms) + res = evaluate.evaluate_problem(problem, state).loss() scores.append(res) # assert monotonic + # assert scores == sorted(scores) # warning: heisenbug. disabled by araistrick + # assert it is strict assert (scores[0] < scores[1]) and scores[1] < scores[2] and scores[2] < scores[3] print("asymmetry scores", scores) @@ -619,6 +781,10 @@ def test_coplanarity(): chair3 = butil.spawn_cube(size=2, location=(8, 0, 0), name='chair3') chair4 = butil.spawn_cube(size=2, location=(12, 0, 0), name='chair4') + obj_states[chair1.name] = ObjectState(chair1, tags= {t.Semantics.Chair}) + obj_states[chair2.name] = ObjectState(chair2, tags= {t.Semantics.Chair}) + obj_states[chair3.name] = ObjectState(chair3, tags= {t.Semantics.Chair}) + obj_states[chair4.name] = ObjectState(chair4, tags= {t.Semantics.Chair}) state = State(objs=obj_states) @@ -633,6 +799,7 @@ def test_coplanarity(): score_terms += [cl.coplanarity_cost(chairs)] problem = cl.Problem(constraints, score_terms) + res1= evaluate.evaluate_problem(problem, state).loss() # print(res1) assert np.isclose(res1, 0, atol=1e-2) @@ -644,6 +811,7 @@ def test_coplanarity(): score_terms = [] score_terms += [cl.coplanarity_cost(chairs)] problem = cl.Problem(constraints, score_terms) + res2 = evaluate.evaluate_problem(problem, state).loss() # print(res2) assert res2 > res1 @@ -653,6 +821,7 @@ def test_coplanarity(): score_terms = [] score_terms += [cl.coplanarity_cost(chairs)] problem = cl.Problem(constraints, score_terms) + res3 = evaluate.evaluate_problem(problem, state).loss() assert res3 > res2 chair2.dimensions = (2, 2, 4) @@ -661,18 +830,67 @@ def test_coplanarity(): score_terms = [] score_terms += [cl.coplanarity_cost(chairs)] problem = cl.Problem(constraints, score_terms) + res4= evaluate.evaluate_problem(problem, state).loss() assert res4 > res3 +def test_evaluate_problem_scalar_ops(): + + state = State(objs={}) + + one = cl.constant(1) + two = cl.constant(2) + three = cl.constant(3) + + e = lambda x: evaluate.evaluate_problem(cl.Problem({}, {repr(x): x}), state).loss() + + assert e(two) == 2 + assert e(one + two) == 3 + assert e(one - two) == -1 + assert e(two * three) == 6 + assert e(two / three) == 2/3 + assert e(two ** three) == 8 + + assert e(two == two) == 1 + assert e(two == one) == 0 + assert e(two >= two) == 1 + assert e(two >= three) == 0 + assert e(two > one) == 1 + assert e(two > two) == 0 + assert e(two <= two) == 1 + assert e(two <= one) == 0 + assert e(two < three) == 1 + assert e(two < two) == 0 + assert e(two != one) == 1 + assert e(two != two) == 0 + + assert e(cl.max_expr(one, two)) == 2 + assert e(cl.min_expr(one, two)) == 1 + assert e(one.clamp_min(two)) == 2 + assert e(two.clamp_max(one)) == 1 + + assert e(-one) == -1 + assert e((-one).abs()) == 1 +def test_evaluate_hinge(): + state = State(objs={}) + e = lambda x: evaluate.evaluate_problem(cl.Problem({}, {repr(x): x}), state).loss() + one = cl.constant(1) + two = cl.constant(2) + assert e(cl.hinge(one, 0, 2)) == 0 + assert e(cl.hinge(one, 1, 2)) == 0 + assert e(cl.hinge(one, 0, 1)) == 0 + assert e(cl.hinge(one, 2, 3)) == 1 + assert e(cl.hinge(two, 0, 1.5)) == 0.5 if __name__ == '__main__': # test_min_dist() + # test_min_dist_tagged() # test_reflection_asymmetry() # test_accessibility_speed() # test_coplanarity() From 02592ffabcb8660e8bb2d3f47c14552d9047be1f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 074/727] Add 83 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/solver/test_constraint_evaluator.py | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/solver/test_constraint_evaluator.py b/tests/solver/test_constraint_evaluator.py index 17da8e9fa..567ac6450 100644 --- a/tests/solver/test_constraint_evaluator.py +++ b/tests/solver/test_constraint_evaluator.py @@ -18,6 +18,7 @@ example_solver as solver, constraint_language as cl ) +from infinigen.core.constraints.evaluator import evaluate from infinigen.core.constraints.evaluator.node_impl import node_impls from infinigen.core.constraints.example_solver.state_def import state_from_dummy_scene, State, ObjectState from infinigen.core.util import blender as butil @@ -38,24 +39,59 @@ def test_home_constraints_implemented(): cons = home_constraints() for node in cons.traverse(): + if node.__class__ in evaluate.SPECIAL_CASE_NODES: continue assert node.__class__ in node_impls def make_chair_table(): + butil.clear_scene() + col = butil.get_collection("indoor_scene_test") + chairs = butil.get_collection("chair") + tables = butil.get_collection("table") + col.children.link(chairs) + col.children.link(tables) + + chair = butil.spawn_cube(size=2, location=(0, 0, 0), name='chair1') + butil.put_in_collection(chair, chairs) + + table = butil.spawn_cube(size=2, location=(3, 0, 0), name='table1') + butil.put_in_collection(table, tables) return col +def test_parse_scene(): butil.clear_scene() + state = state_from_dummy_scene(make_chair_table()) + + assert state.objs['chair1'].tags == {t.Semantics.Chair, t.SpecificObject('chair1')} + assert state.objs['table1'].tags == {t.Semantics.Table, t.SpecificObject('table1')} + +def test_eval_node(): butil.clear_scene() + + state = state_from_dummy_scene(make_chair_table()) + eval = partial(evaluate.evaluate_node, state=state) + + scene = cl.scene() + assert eval(scene) == {"chair1", "table1"} + + assert eval(scene.tagged({t.Semantics.Chair})) == {'chair1'} + assert eval(scene.tagged({t.Semantics.Seating})) == set() + assert eval(scene.tagged({t.Semantics.Chair}).count()) == 1 + def test_min_dist(): butil.clear_scene() col = make_chair_table() + state = state_from_dummy_scene(col) constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) + sofa = cl.tagged(scene, {t.Semantics.Seating}) score_terms += [cl.distance(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -66,7 +102,10 @@ def test_min_dist(): problem = cl.Problem(constraints, score_terms) assert np.isclose(evaluate.evaluate_problem(problem, state).loss(), 2) + s = butil.spawn_cube(size=2, location=(-4, 0, 0), name="sofa1") sofas = butil.get_collection("seating") + col.children.link(sofas) + butil.put_in_collection(s, sofas) score_terms = [] score_terms += [cl.distance(chair, sofa) + cl.distance(chair, table)] @@ -106,6 +145,8 @@ def test_accessibility_monotonicity(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.accessibility_cost(chair, table, dist=3)] problem = cl.Problem(constraints, score_terms) @@ -142,6 +183,8 @@ def test_accessibility_side(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -172,6 +215,8 @@ def test_accessibility_angle(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -201,6 +246,8 @@ def test_accessibility_volume(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.accessibility_cost(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -230,6 +277,8 @@ def test_accessibility_volume(): # constraints = [] # score_terms = [] # scene = cl.scene() +# chair = cl.tagged(scene, {t.Semantics.Chair}) +# table = cl.tagged(scene, {t.Semantics.Table}) # score_terms += [cl.accessibility_cost(chair, table)] # problem = cl.Problem(constraints, score_terms) @@ -264,6 +313,8 @@ def test_angle_alignment(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.angle_alignment_cost(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -302,6 +353,8 @@ def test_angle_alignment_multiple_objects(): score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + tables = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.angle_alignment_cost(chair, tables)] @@ -348,6 +401,8 @@ def test_angle_alignment_multiple_objects_varying_positions(): score_terms = [] scene = cl.scene() + chair_obj = cl.tagged(scene, {t.Semantics.Chair}) + table_objs = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] @@ -410,6 +465,8 @@ def test_angle_alignment_multipolygon_projection(): score_terms = [] scene = cl.scene() + chair_obj = cl.tagged(scene, {t.Semantics.Chair}) + table_objs = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.angle_alignment_cost(chair_obj, table_objs)] @@ -440,7 +497,10 @@ def test_angle_alignment_tagged(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) + score_terms += [cl.angle_alignment_cost(chair, table, others_tags={t.Subpart.Front})] problem = cl.Problem(constraints, score_terms) res = evaluate.evaluate_problem(problem, state).loss() @@ -463,7 +523,10 @@ def test_angle_alignment_tagged(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) + score_terms += [cl.angle_alignment_cost(chair, table, others_tags={t.Subpart.Front})] problem = cl.Problem(constraints, score_terms) res = evaluate.evaluate_problem(problem, state).loss() @@ -493,6 +556,8 @@ def test_focus_score(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) score_terms += [cl.focus_score(chair, table)] problem = cl.Problem(constraints, score_terms) @@ -517,22 +582,26 @@ def mk_state(n): return State(objs=obj_states) + cons = cl.Problem([cl.scene().tagged(t.Semantics.Furniture).count().in_range(1, 3)], []) assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 1 assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(7))[1] == 4 + cons = cl.Problem([cl.scene().tagged(t.Semantics.Furniture).count() <= 3], []) assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(4))[1] == 1 assert evaluate.evaluate_problem(cons, mk_state(6))[1] == 3 + cons = cl.Problem([cl.scene().tagged(t.Semantics.Furniture).count() >= 3], []) assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 3 assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 2 assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(4))[1] == 0 assert evaluate.evaluate_problem(cons, mk_state(6))[1] == 0 + cons = cl.Problem([cl.scene().tagged(t.Semantics.Furniture).count() == 3], []) assert evaluate.evaluate_problem(cons, mk_state(0))[1] == 3 assert evaluate.evaluate_problem(cons, mk_state(1))[1] == 2 assert evaluate.evaluate_problem(cons, mk_state(3))[1] == 0 @@ -558,9 +627,12 @@ def test_min_dist_tagged(): constraints = [] score_terms = [] scene = cl.scene() + chair = cl.tagged(scene, {t.Semantics.Chair}) + table = cl.tagged(scene, {t.Semantics.Table}) + score_terms += [cl.distance(chair, table, others_tags={t.Subpart.Front})] problem = cl.Problem(constraints, score_terms) res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 4) @@ -568,8 +640,11 @@ def test_min_dist_tagged(): constraints = [] score_terms = [] scene = cl.scene() + # chair = cl.tagged(scene, {t.Semantics.Chair}) + # table = cl.tagged(scene, {t.Semantics.Table}) # butil.save_blend('table_chair.blend') + score_terms += [cl.distance(chair, table, others_tags={t.Subpart.Top})] problem = cl.Problem(constraints, score_terms) res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 2) @@ -577,7 +652,10 @@ def test_min_dist_tagged(): constraints = [] score_terms = [] scene = cl.scene() + # chair = cl.tagged(scene, {t.Semantics.Chair}) + # table = cl.tagged(scene, {t.Semantics.Table}) + score_terms += [cl.distance(chair, table, others_tags={t.Subpart.Bottom})] problem = cl.Problem(constraints, score_terms) res = evaluate.evaluate_problem(problem, state).loss() assert np.isclose(res, 6) @@ -606,6 +684,10 @@ def test_table(): bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') tagging.tag_canonical_surfaces(obj) + tagging.extract_tagged_faces(obj, {t.Subpart.Front}) + tagging.extract_tagged_faces(obj, {t.Subpart.Top}) + tagging.extract_tagged_faces(obj, {t.Subpart.Bottom}) + tagging.extract_tagged_faces(obj, {t.Subpart.Back}) # butil.save_blend('table.blend') # obj_tags = tagging.union_object_tags(obj) @@ -796,6 +878,7 @@ def test_coplanarity(): constraints = [] score_terms = [] scene = cl.scene() + chairs = cl.tagged(scene, {t.Semantics.Chair}) score_terms += [cl.coplanarity_cost(chairs)] problem = cl.Problem(constraints, score_terms) From 8edfdb420ca8001b5d4503083769748847a8ff8a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 075/727] Add 56 lines to tests/solver/test_asset_surfaces.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_asset_surfaces.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/solver/test_asset_surfaces.py diff --git a/tests/solver/test_asset_surfaces.py b/tests/solver/test_asset_surfaces.py new file mode 100644 index 000000000..007a0804f --- /dev/null +++ b/tests/solver/test_asset_surfaces.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 +import pytest + +from infinigen.core.constraints import ( + usage_lookup, + constraint_language as cl +) + +from infinigen.core.constraints.example_solver.geometry import dof + +from infinigen_examples.indoor_constraint_examples import home_asset_usage + +from infinigen.core.util import blender as butil + +def test_canonical_planes_real_placeholders(): + + used_as = home_asset_usage() + usage_lookup.initialize_from_dict(used_as) + + test_facs = pholder_facs.union(asset_facs) + + test_facs.intersection_update( + ) + + for fac in test_facs: + butil.clear_scene() + + if fac in pholder_facs: + obj = fac(0).spawn_placeholder(0, loc=(0,0,0), rot=(0,0,0)) + elif fac in asset_facs: + obj = fac(0).spawn_asset(0, loc=(0,0,0), rot=(0,0,0)) + else: + raise ValueError() + + 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') + + tagging.tag_canonical_surfaces(obj) + + obj_tags = tagging.union_object_tags(obj) + + for tag in [t.Subpart.Back, t.Subpart.Bottom]:#, t.Subpart.SupportSurface]: + mask = tagging.tagged_face_mask(obj, {tag}) + if mask.sum() == 0: + obj_tags = tagging.union_object_tags(obj) + raise ValueError( + f'{obj.name=} has nothing tagged for {tag=}. {obj_tags=}' + ) \ No newline at end of file From c198d8961f0d94ac9f1d708816ca6ba619de1e20 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 076/727] Add 5 lines to tests/solver/test_asset_surfaces.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/solver/test_asset_surfaces.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/solver/test_asset_surfaces.py b/tests/solver/test_asset_surfaces.py index 007a0804f..94b7c0c3a 100644 --- a/tests/solver/test_asset_surfaces.py +++ b/tests/solver/test_asset_surfaces.py @@ -17,15 +17,20 @@ from infinigen_examples.indoor_constraint_examples import home_asset_usage from infinigen.core.util import blender as butil +from infinigen.core import tagging, tags as t def test_canonical_planes_real_placeholders(): used_as = home_asset_usage() usage_lookup.initialize_from_dict(used_as) + pholder_facs = usage_lookup.factories_for_usage({t.Semantics.RealPlaceholder}) + asset_facs = usage_lookup.factories_for_usage({t.Semantics.AssetAsPlaceholder}) test_facs = pholder_facs.union(asset_facs) test_facs.intersection_update( + usage_lookup.factories_for_usage({t.Semantics.Storage}) + .union(usage_lookup.factories_for_usage({t.Semantics.Seating})) ) for fac in test_facs: From cb7481580217bd6a91393cb5cdea07a0feba6881 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 077/727] Add 471 lines to tests/solver/test_greedy_partition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_greedy_partition.py | 471 ++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 tests/solver/test_greedy_partition.py diff --git a/tests/solver/test_greedy_partition.py b/tests/solver/test_greedy_partition.py new file mode 100644 index 000000000..2518af262 --- /dev/null +++ b/tests/solver/test_greedy_partition.py @@ -0,0 +1,471 @@ +# Copyright (c) Princeton University. +# This 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 copy +import pytest + +from pprint import pprint + +from infinigen.core import tags as t + +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r, + usage_lookup, + evaluator +) +from infinigen.core.constraints.example_solver import ( + state_def, + greedy, + propose_discrete, +) + +from infinigen_examples import indoor_constraint_examples as ex, generate_indoors +from infinigen_examples.util import constraint_util as cu + +from test_greedy_substitutions import make_dummy_state + +def test_partition_basecase_irrelevant(): + cons = cl.scene()[t.Semantics.TableDisplayItem].count().in_range(0, 1) + res, relevant = greedy.filter_constraints(cons, r.Domain({t.Semantics.Room}, [])) + assert not relevant + +def test_basecase_relevant(): + cons = cl.scene()[t.Semantics.Room].count().in_range(0, 1) + filter = r.Domain({t.Semantics.Room}, []) + res, relevant = greedy.filter_constraints(cons, filter) + assert relevant + +def test_partition_collapse_binop(): + + cons = ( + cl.scene()[t.Semantics.Furniture].count().in_range(0, 1) * + cl.scene()[t.Semantics.Room].count().in_range(0, 1) + ) + + assert not greedy.filter_constraints(cons.operands[0], r.Domain({t.Semantics.Room}, []))[1] + assert greedy.filter_constraints(cons.operands[1], r.Domain({t.Semantics.Room}, []))[1] + + res, relevant = greedy.filter_constraints(cons, r.Domain({t.Semantics.Room}, [])) + assert relevant + + print("RES", res) + + expect = cl.scene()[t.Semantics.Room].count().in_range(0, 1) + assert r.expr_equal(res, expect) + + +def test_partition_eliminate_irrelevant(): + + scene = cl.scene() + firstpart = scene[t.Semantics.Furniture].count().in_range(0, 1) + secondpart = scene[t.Semantics.Furniture].all(lambda f: ( + scene[t.Semantics.Chair].related_to(f, cl.AnyRelation()).count().in_range(0, 1) + )) + cons = firstpart * secondpart + + assert not greedy.filter_constraints(secondpart, r.Domain({t.Semantics.Furniture}, []))[1] + + res, relevant = greedy.filter_constraints(cons, r.Domain({t.Semantics.Furniture})) + assert relevant + assert r.expr_equal(res, firstpart) + +def test_greedy_partition_bathroom(): + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + bath_cons = prob.constraints['bathroom'] + + on_floor = stages['on_floor'] + on_floor_any = r.domain_tag_substitute(on_floor, cu.variable_room, r.Domain()) + assert greedy.filter_constraints(bath_cons, on_floor_any)[1] + + on_bathroom = r.domain_tag_substitute(on_floor, cu.variable_room, r.Domain({t.Semantics.Bathroom})) + assert greedy.filter_constraints(bath_cons, on_bathroom)[1] + +def test_greedy_partition_multilevel(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + bathroom = cl.scene()[{t.Semantics.Room, t.Semantics.Bathroom}].excludes(cu.room_types) + storage = cl.scene()[t.Semantics.Storage] + + bath_cons_1 = storage.related_to(bathroom, cu.on_floor).count().in_range(0, 1) + + on_hallway = r.domain_tag_substitute(stages['on_floor'], cu.variable_room, r.Domain({t.Semantics.Hallway})) + assert not greedy.filter_constraints(bath_cons_1, on_hallway)[1] + + bath_cons_2 = bathroom.all(lambda r: storage.related_to(r, cu.on_floor).count().in_range(0, 1)) + assert not greedy.filter_constraints(bath_cons_2, on_hallway)[1] + + bath_cons_3 = bathroom.all(lambda r: ( + storage.related_to(r).all( + lambda s: cl.scene()[t.Semantics.Object].related_to(s).count().in_range(0, 1) + ) + )) + assert not greedy.filter_constraints(bath_cons_3, on_hallway)[1] + +def test_greedy_partition_bathroom_nofalsepositive(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + bath_cons = prob.constraints['bathroom'] + + on_hallway = r.domain_tag_substitute(stages['on_floor'], cu.variable_room, r.Domain({t.Semantics.Hallway})) + assert not greedy.filter_constraints(bath_cons, on_hallway)[1] + +def test_greedy_partition_plants(): + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + plant_cons = prob.constraints['plants'] + + on_floor = stages['on_floor'] + on_floor_any = r.domain_tag_substitute(on_floor, cu.variable_room, r.Domain()) + assert greedy.filter_constraints(plant_cons, on_floor_any)[1] + + on_bathroom = r.domain_tag_substitute(on_floor, cu.variable_room, r.Domain({t.Semantics.Bathroom})) + assert greedy.filter_constraints(plant_cons, on_bathroom)[1] + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_objects_on_generic_obj(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + stages = generate_indoors.default_greedy_stages() + + on_obj = stages['on_obj'] + on_obj = r.domain_tag_substitute(on_obj, cu.variable_room, r.Domain()) + on_obj = r.domain_tag_substitute( + on_obj, cu.variable_obj, r.Domain({t.SpecificObject('thatchair'), t.Semantics.Chair}) + ) + print("ON_OBJ_FILTER", on_obj) + + bathroom = cl.scene()[t.Semantics.Room, t.Semantics.Bathroom] + storage = cl.scene()[t.Semantics.Object, t.Semantics.Storage] + prob = bathroom.all(lambda r: + storage.related_to(r).all(lambda s: ( + cl.scene()[t.Semantics.Object].related_to(s).count().in_range(0, 1) + )) + ) + + cons, relevant = greedy.filter_constraints(prob, on_obj) + assert not relevant + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_on_obj_coverage(): + + cons = cl.scene()[t.Semantics.Room].all(lambda r: ( + cl.scene()[t.Semantics.Storage].related_to(r).all(lambda s: ( + cl.scene()[t.Semantics.Object].related_to(s).count().in_range(0, 1) + )) + )) + + obj_in_bathroom = r.domain_tag_substitute( + generate_indoors.default_greedy_stages()['on_obj'], + cu.variable_room, + r.Domain({t.Semantics.Bathroom}) + ) + obj_in_bathroom = r.domain_tag_substitute( + obj_in_bathroom, cu.variable_obj, r.Domain({t.Semantics.Storage}) + ) + + res, relevant = greedy.filter_constraints(cons, obj_in_bathroom) + assert relevant + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_only_bathcons_coverage(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + bath_cons = prob.constraints['bathroom'] + + dom = r.domain_tag_substitute( + stages['on_floor'], cu.variable_room, r.Domain({t.Semantics.Bathroom}) + ) + assert greedy.filter_constraints(bath_cons, dom)[1] + + dom = r.domain_tag_substitute( + stages['on_wall'], cu.variable_room, r.Domain({t.Semantics.Bathroom}) + ) + assert greedy.filter_constraints(bath_cons, dom)[1] + + dom = r.domain_tag_substitute( + stages['on_obj'], cu.variable_room, r.Domain({t.Semantics.Bathroom}) + ) + dom = r.domain_tag_substitute( + dom, cu.variable_obj, r.Domain({t.Semantics.Storage}) + ) + assert greedy.filter_constraints(bath_cons, dom)[1] + +@pytest.fixture +def precompute_all_coverage(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + stages = generate_indoors.default_greedy_stages() + + cons_coverage = {k: set() for k in prob.constraints.keys()} + score_coverage = {k: set() for k in prob.score_terms.keys()} + + for k, filter in stages.items(): + + for roomtype in cu.room_types: + room_filter = r.domain_tag_substitute( + copy.deepcopy(filter), cu.variable_room, r.Domain({roomtype}) + ) + + # eliminate the var, assume any object is fine, most generous possible assumption + room_filter = r.domain_tag_substitute( + room_filter, cu.variable_obj, r.Domain() + ) + for name, cons in prob.constraints.items(): + if greedy.filter_constraints(cons, room_filter)[1]: + cons_coverage[name].add((k, roomtype)) + for name, score in prob.score_terms.items(): + if greedy.filter_constraints(score, room_filter)[1]: + score_coverage[name].add((k, roomtype)) + + return cons_coverage, score_coverage + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_specific_coverage(precompute_all_coverage): + + cons_coverage, _ = precompute_all_coverage + + assert cons_coverage['bathroom'] == { + ('on_floor', t.Semantics.Bathroom), + ('on_wall', t.Semantics.Bathroom), + ('on_obj', t.Semantics.Bathroom), + } + + assert cons_coverage['diningroom'] == { + ('on_floor', t.Semantics.DiningRoom), + ('on_wall', t.Semantics.DiningRoom), + ('on_obj', t.Semantics.DiningRoom), + } + + assert cons_coverage['livingroom'] == { + ('on_floor', t.Semantics.LivingRoom), + ('on_wall', t.Semantics.LivingRoom), + ('on_obj', t.Semantics.LivingRoom), + } + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_greedy_partition_coverage(precompute_all_coverage): + + cons_coverage, score_coverage = precompute_all_coverage + + for k, v in cons_coverage.items(): + if len(cons_coverage[k]) == 0: + raise ValueError(f"Constraint {k} has no coverage") + for k, v in score_coverage.items(): + if len(score_coverage[k]) == 0: + raise ValueError(f"Score term {k} has no coverage") + +def get_on_diningroom_stage(): + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + stages = generate_indoors.default_greedy_stages() + on_diningroom = r.domain_tag_substitute( + stages['on_floor'], + cu.variable_room, + r.Domain({t.Semantics.DiningRoom, t.Semantics.Room}) + ) + return on_diningroom + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_greedy_partition_diningroom(): + + on_diningroom = get_on_diningroom_stage() + prob = ex.home_constraints() + diningroom = prob.constraints['diningroom'] + + + for node in diningroom.traverse(): + if isinstance(node, cl.item): + print(node) + + res, relevant = greedy.filter_constraints(diningroom, on_diningroom) + assert relevant + + print("FILTER", on_diningroom) + print("RES", res) + + assert isinstance(res, cl.ForAll) + assert res.pred.__class__ is not cl.constant + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_diningroom_bounds_active(): + + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + stages = generate_indoors.default_greedy_stages() + on_diningroom = r.domain_tag_substitute( + stages['on_floor'], cu.variable_room, r.Domain({t.Semantics.DiningRoom}) + ) + + prob = ex.home_constraints() + diningroom = prob.constraints['diningroom'] + + bounds_before_preproc = r.constraint_bounds(diningroom) + bounds = propose_discrete.preproc_bounds( + bounds_before_preproc, state_def.State({}), on_diningroom + ) + + assert len(bounds) > 0 + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_partition_keep_constants(): + + cons = (cl.scene()[t.Semantics.Room].count() * 2) + res, relevant = greedy.filter_constraints(cons, r.Domain({t.Semantics.Room}, [])) + assert relevant + assert r.expr_equal(res, cons) + +@pytest.mark.skip # filter_constraints development has been abandoned until a later date +def test_multiroom_viol(): + + state = make_dummy_state({ + (t.Semantics.Room,): 3 + }) + + state.objs['room_0'].tags.add(t.Semantics.DiningRoom) + + cons = cl.scene()[t.Semantics.Room].all(lambda r: + cl.scene()[{t.Semantics.Object, t.Semantics.Storage}].related_to(r, cu.on_floor).count() == 1 + ) + prob = cl.Problem({'storage': cons}, {}) + + on_diningroom = get_on_diningroom_stage() + prob, relevant = greedy.filter_constraints(prob, on_diningroom) + assert relevant + + print("PRED", prob.constraints['storage'].pred) + print("OBJS", prob.constraints['storage'].objs) + + result = evaluator.evaluate_problem(prob, state) + assert result.viol_count() == 1 # only one room is relevant, so only one violation applies for this stage + + state.objs['stor_1'] = state_def.ObjectState( + obj=None, + generator=None, + tags={t.Semantics.Storage}, + relations=[ + state_def.RelationState( + relation=cl.StableAgainst(), + target_name='room_0' + ) + ] + ) + + result = evaluator.evaluate_problem(prob, state) + assert result.viol_count() == 0 # only one room is relevant, and it has an obj + +@pytest.mark.skip +def test_forall_furnroom(): + + scene = cl.scene() + rooms = scene[t.Semantics.Room] + furniture = scene[t.Semantics.Furniture] + + cons = rooms.all( + lambda r: furniture.related_to(r).count().in_range(0, 1) + ) + + room = r.Domain({t.Semantics.Room}, []) + furn = r.Domain({t.Semantics.Furniture}, []) + furn_room = furn.with_relation(cl.AnyRelation(), room) + furn_no_room = furn.with_relation(-cl.AnyRelation(), room) + + res, rel = greedy.filter_constraints(cons, furn) + assert rel + assert r.expr_equal(res, cons) + + res, rel = greedy.filter_constraints(cons, furn_room) + assert rel + assert r.expr_equal(res, cons) + + res, rel = greedy.filter_constraints(cons, furn_no_room) + assert not rel + +@pytest.mark.skip +def test_forall_narrow_pred(): + + scene = cl.scene() + rooms = scene[t.Semantics.Room] + furniture = scene[t.Semantics.Furniture] + + cons = rooms.all( + lambda r: furniture.related_to(r).count().in_range(0, 1) + ) + + room = r.Domain({t.Semantics.Room}, []) + stor = r.Domain({t.Semantics.Furniture}, []) + stor_room = stor.with_relation(cl.AnyRelation(), room) + stor_no_room = stor.with_relation(-cl.AnyRelation(), room) + + res, rel = greedy.filter_constraints(cons, stor) + assert rel + assert r.expr_equal(res, cons) + + res, rel = greedy.filter_constraints(cons, stor_room) + assert rel + assert r.expr_equal(res, cons) + + res, rel = greedy.filter_constraints(cons, stor_no_room) + assert not rel + +@pytest.mark.skip +def test_forall_narrow_loopvar(): + + scene = cl.scene() + rooms = scene[t.Semantics.Room] + furniture = scene[t.Semantics.Furniture] + + cons = rooms.all( + lambda r: furniture.related_to(r).count().in_range(0, 1) + ) + + droom = r.Domain({t.Semantics.Room, t.Semantics.DiningRoom}, []) + furn = r.Domain({t.Semantics.Furniture}, []) + furn_room = furn.with_relation(cl.AnyRelation(), droom) + furn_no_room = furn.with_relation(-cl.AnyRelation(), droom) + + + cons_narrow = r.FilterByDomain(rooms, droom).all( + lambda r: furniture.related_to(r).count().in_range(0, 1) + ) + + res, rel = greedy.filter_constraints(cons, furn) + print(res) + assert rel + assert r.expr_equal(res, cons_narrow) + + res, rel = greedy.filter_constraints(cons, furn_room) + assert rel + assert r.expr_equal(res, cons_narrow) + + res, rel = greedy.filter_constraints(cons, furn_no_room) + assert not rel + +@pytest.mark.skip +def test_forall_sumconst(): + + scene = cl.scene() + rooms = scene[t.Semantics.Room] + furniture = scene[t.Semantics.Furniture] + + sumcons = rooms.sum(lambda r: cl.constant(1)) + assert greedy.filter_constraints(sumcons, r.Domain({t.Semantics.Room}))[1] + + + From 265f6dc003301c8187ca8fa2dc793e0cb37fcfb5 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 078/727] Add 120 lines to tests/solver/test_greedy_substitutions.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_greedy_substitutions.py | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/solver/test_greedy_substitutions.py diff --git a/tests/solver/test_greedy_substitutions.py b/tests/solver/test_greedy_substitutions.py new file mode 100644 index 000000000..c499628ba --- /dev/null +++ b/tests/solver/test_greedy_substitutions.py @@ -0,0 +1,120 @@ +# Copyright (c) Princeton University. +# This 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.core import tags as t + +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r +) +from infinigen.core.constraints.example_solver import ( + state_def, + greedy +) + +def make_dummy_state(type_counts: dict[tuple[t.Tag], int]): + + objs = {} + for tags, count in type_counts.items(): + for i in range(count): + name = '_'.join([t.value for t in tags]) + f'_{i}' + objs[name] = state_def.ObjectState( + obj=None, + generator=None, + tags=set(tags).union([t.SpecificObject(name)]), + relations=[] + ) + + return state_def.State( + objs=objs + ) + +def test_substitutions_no_vars(): + state = make_dummy_state({ + (t.Semantics.Room,): 3, + }) + + var_dom = r.Domain( + {t.Semantics.Room}, + [] + ) + + subs = list(greedy.substitutions(var_dom, state)) + assert len(subs) == 1 + +def test_substitutions_simple(): + state = make_dummy_state({ + (t.Semantics.Room,): 3, + }) + + var_dom = r.Domain( + {t.Semantics.Room, t.Variable('room')}, + [] + ) + + subs = list(greedy.substitutions(var_dom, state)) + assert len(subs) == 3 + assert t.SpecificObject('room_0') in subs[0].tags + assert t.SpecificObject('room_1') in subs[1].tags + assert t.SpecificObject('room_2') in subs[2].tags + +def test_substitutions_child(): + + state = make_dummy_state({ + (t.Semantics.Room,): 4, + }) + + var_dom = r.Domain( + {t.Semantics.Object}, + [ + (cl.AnyRelation(), r.Domain({t.Semantics.Room, t.Variable('room')}, [])) + ] + ) + + subs = list(greedy.substitutions(var_dom, state)) + assert len(subs) == 4 + + +def test_substitutions_child_complex(): + + state = make_dummy_state({ + (t.Semantics.Room,): 4, + }) + + state.objs['obj_0'] = state_def.ObjectState( + obj=None, + generator=None, + tags={t.Semantics.Object, t.SpecificObject('obj_0')}, + relations=[ + state_def.RelationState( + relation=cl.Touching(), target_name='room_0' + ) + ] + ) + + state.objs['obj_1'] = state_def.ObjectState( + obj=None, + generator=None, + tags={t.Semantics.Object, t.SpecificObject('obj_1')}, + relations=[ + state_def.RelationState( + relation=cl.Touching(), target_name='room_1' + ) + ] + ) + + var_dom = r.Domain( + {t.Semantics.Object, t.Variable('obj')}, + [ + (cl.AnyRelation(), r.Domain({t.Semantics.Room, t.Variable('room')}, [])), + ] + ) + + subs = list(greedy.substitutions(var_dom, state)) + print(subs) + assert len(subs) == 2 + assert len([s for s in subs if t.SpecificObject('obj_0') in s.tags]) == 1 + assert len([s for s in subs if t.SpecificObject('obj_1') in s.tags]) == 1 \ No newline at end of file From 2167d8f887a17f204f1eb00fe2dd0bab4a861acd Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 079/727] Add 44 lines to tests/solver/test_state_def.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_state_def.py | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/solver/test_state_def.py diff --git a/tests/solver/test_state_def.py b/tests/solver/test_state_def.py new file mode 100644 index 000000000..8b03fd67b --- /dev/null +++ b/tests/solver/test_state_def.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: Alexander Raistrick + +from itertools import chain +from functools import partial +import json + +# import pytest +import bpy +import numpy as np +import sys +import os + +from infinigen.core.constraints.example_solver.geometry import dof, parse_scene, planes, stability, validity +from mathutils import Vector + +from infinigen.core.constraints import ( + usage_lookup, + example_solver as solver, + constraint_language as cl +) +from infinigen.core import tagging, tags as t +from infinigen.core.util import blender as butil +from infinigen.core.constraints.example_solver import ( + state_def +) + +from test_stable_against import make_scene + +def test_state_to_json(tmp_path): + + state = make_scene(Vector((1, 0, 0))) + + path = tmp_path/'state.json' + state.to_json(path) + + with path.open() as json_file: + state_json = json.load(json_file) + + assert sorted(list(state_json['objs'].keys())) == ['cup', 'table'] + assert len(state_json['objs']['cup']['relations']) == 1 \ No newline at end of file From b14abcaf1935e4dba9a08655560bba78a5e8ea71 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 080/727] Add 286 lines to tests/solver/test_greedy_stages.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/solver/test_greedy_stages.py | 286 +++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 tests/solver/test_greedy_stages.py diff --git a/tests/solver/test_greedy_stages.py b/tests/solver/test_greedy_stages.py new file mode 100644 index 000000000..2ec5ecf4a --- /dev/null +++ b/tests/solver/test_greedy_stages.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: Alexander Raistrick + +import logging + +from pprint import pprint +import pytest + +from infinigen.core import tags as t +from infinigen.core.constraints import ( + checks, + constraint_language as cl, + reasoning as r, + usage_lookup, + evaluator +) +from infinigen.core.constraints.example_solver import ( + propose_discrete, + greedy, + state_def +) +from infinigen_examples import indoor_constraint_examples as ex, generate_indoors +from infinigen_examples.util import constraint_util as cu + +from infinigen.assets.tableware import PlantContainerFactory + +from infinigen.core.util import blender as butil + +@pytest.mark.parametrize('key', generate_indoors.default_greedy_stages().keys()) + + pprint(generate_indoors.default_greedy_stages()) + + v = generate_indoors.default_greedy_stages()[key] + assert not v.is_recursive() + + if len(v.relations) != 0: + + if all(isinstance(r, cl.NegatedRelation) for r, _ in v.relations): + raise ValueError(f"Stage {key} has no positive relation, {[r for r, _ in v.relations]}") + #if any(isinstance(r, cl.AnyRelation) for r, _ in v.relations): + # raise ValueError(f"Stage {key} has an AnyRelation which is underspecified, {v}") +#@pytest.mark.parametrize('key', generate_indoors.default_greedy_stages().keys()) +#@pytest.mark.parametrize('roomtype', cu.room_types) +#def test_stage_bound_roomsubs(key: str, roomtype: t.Semantics): +# +# stages = generate_indoors.default_greedy_stages() +# stage = stages[key] +# stage = r.domain_tag_substitute(stage, t.Variable('room'), r.Domain({roomtype})) +# +# bounds = r.constraint_bounds(ex.home_constraints()) + +def test_validate_bounds(): + + bounds = r.constraint_bounds(ex.home_constraints()) + + for b in bounds: + for rel, dom in b.domain.relations: + if not r.domain_finalized(dom, check_anyrel=False, check_variable=True): + raise ValueError(f"Unfinalized domain {dom=}") + if isinstance(rel, cl.GeometryRelation): + if rel.child_tags == set(): + raise ValueError(f"GeometryRelation with empty child_tags in {b=}") + if rel.parent_tags == set(): + raise ValueError(f"GeometryRelation with empty parent_tags in {b=}") + +def test_validate_stages(): + stages = generate_indoors.default_greedy_stages() + + wall = stages['on_wall'] + floor = stages['on_floor'] + assert not wall.intersects(floor) + + onobj = stages['obj_ontop_obj'] + assert not onobj.intersects(floor) + + checks.validate_stages(stages) + +def test_example_intersects(): + + on_wall_complex = cl.StableAgainst( + ) + assert on_wall_simple.intersects(on_wall_complex) + + dom = r.Domain( + relations=[ + ] + ) + + filter = generate_indoors.default_greedy_stages()['on_wall'] + assert propose_discrete.active_for_stage(dom, filter) + +def test_contradiction_fail(): + + prob = cl.Problem(constraints=[ + ], score_terms=[]) + with pytest.raises(ValueError): + checks.check_contradictory_domains(prob) + +def get_walldec(): + return r.Domain( +def test_example_walldec(): + + dom = get_walldec() + assert not propose_discrete.active_for_stage(dom, stages['on_ceiling']) + assert not propose_discrete.active_for_stage(dom, stages['on_floor']) + + assert t.satisfies(dom.tags, stages['on_wall'].tags) + print("ONWALL", stages['on_wall']) + + assert propose_discrete.active_for_stage(dom, stages['on_wall']) + +def test_example_floorwall(): + + on = cl.StableAgainst( + {t.Subpart.Bottom, -t.Subpart.Front, -t.Subpart.Top, -t.Subpart.Back}, + {t.Subpart.SupportSurface, t.Subpart.Visible, -t.Subpart.Wall, -t.Subpart.Ceiling} + ) + + against = cl.StableAgainst( + {t.Subpart.Back, -t.Subpart.Top, -t.Subpart.Front}, + {t.Subpart.Visible, t.Subpart.Wall, -t.Subpart.SupportSurface, -t.Subpart.Ceiling} + ) + + dom = r.Domain( + {t.Semantics.Storage, t.Semantics.Furniture, t.Semantics.Object, -t.Semantics.Room}, + [ + (on, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])), + (against, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + ] + ) + + stages = generate_indoors.default_greedy_stages() + + assert propose_discrete.active_for_stage(dom, stages['on_floor']) + assert not propose_discrete.active_for_stage(dom, stages['on_wall']) + +def test_example_secondary(): + + + floorwall_furn = r.Domain({t.Semantics.Furniture, t.Semantics.Storage, t.Semantics.Object}, [ + (cu.on_floor, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])), + (cu.against_wall, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + ]) + dom = r.Domain({t.FromGenerator(PlantContainerFactory), t.Semantics.Object, -t.Semantics.Room}, [ + (cl.StableAgainst({t.Subpart.Bottom}, {t.Subpart.Top}), floorwall_furn) + ]) + + stages = generate_indoors.default_greedy_stages() + on_obj = stages['obj_ontop_obj'] + assert propose_discrete.active_for_stage(dom, on_obj) + +def test_example_sideobj(): + + anyroom = r.Domain({t.Semantics.Room, -t.Semantics.Object}, []) + + objonroom = r.Domain({t.Semantics.Object, t.Semantics.Table, -t.Semantics.Room}, [ + (cu.on_floor, anyroom) + ]) + + dom = r.Domain({t.Semantics.Object, t.Semantics.Chair, -t.Semantics.Room}, [ + (cu.front_against, objonroom), + (cu.on_floor, anyroom) + ]) + stages = generate_indoors.default_greedy_stages() + assert propose_discrete.active_for_stage(dom, stages['side_obj']) + assert not propose_discrete.active_for_stage(dom, stages['on_floor']) + +def test_example_monitor(): + + desk = r.Domain({t.Semantics.Object, t.Semantics.Desk, -t.Semantics.Room}, [ + (cu.on_floor, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])), + (cu.against_wall, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + ]) + + monitor = r.Domain({t.Semantics.Object, t.Semantics.Chair, -t.Semantics.Room}, [ # chair vs other tags doesnt matter + (cu.ontop, desk), + (cu.against_wall, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + ]) + + stages = generate_indoors.default_greedy_stages() + + assert propose_discrete.active_for_stage(monitor, stages['obj_ontop_obj']) + assert not propose_discrete.active_for_stage(monitor, stages['on_wall']) + +def test_example_on_obj(): + + not_others = {-t.Semantics.LivingRoom, -t.Semantics.Hallway, -t.Semantics.Closet, -t.Semantics.Balcony, -t.Semantics.Staircase, -t.Semantics.Garage, -t.Semantics.DiningRoom, -t.Semantics.Utility, -t.Semantics.Bathroom, -t.Semantics.Kitchen} + bedroom_storage = r.Domain({t.Semantics.Object, t.Semantics.Furniture, t.Semantics.Storage}, [ + (cu.on_floor, r.Domain({t.Semantics.Bedroom, t.Semantics.Room}.union(not_others), [])), + (cu.against_wall, r.Domain({t.Semantics.Bedroom, t.Semantics.Room}.union(not_others), [])) + ]) + + obj = r.Domain({t.Semantics.OfficeShelfItem, t.Semantics.Object}, [ + (cu.on, bedroom_storage) + ]) + + onfloor = generate_indoors.default_greedy_stages()['on_floor'] + dining = r.domain_tag_substitute(onfloor, cu.variable_room, r.Domain({t.Semantics.DiningRoom})) + + assert not propose_discrete.active_for_stage(obj, dining) + +def test_active_incorrect_room(): + + onfloor = generate_indoors.default_greedy_stages()['on_floor'] + dining = r.domain_tag_substitute(onfloor, cu.variable_room, r.Domain({t.Semantics.DiningRoom})) + + sofa = r.Domain({t.Semantics.Object, t.Semantics.Seating, -t.Semantics.Room}, [ + (cu.on_floor, r.Domain({t.Semantics.LivingRoom, -t.Semantics.DiningRoom, -t.Semantics.Object}, [])) + ]) + + assert not propose_discrete.active_for_stage(sofa, dining) + +def test_stage_intersect_table(): + + onfloor = generate_indoors.default_greedy_stages()['on_floor'] + onfloor_dining = r.domain_tag_substitute(onfloor, cu.variable_room, r.Domain({t.Semantics.DiningRoom, t.SpecificObject('diningroom01')})) + + dining = r.Domain({t.Semantics.Room, t.Semantics.DiningRoom, -t.Semantics.Object}, []) + table = r.Domain({t.Semantics.Object, t.Semantics.Table, -t.Semantics.Room}, [ + (cu.on_floor, dining) + ]) + + inter = onfloor_dining.intersection(table) + assert len(inter.relations) == 2 + assert inter.relations[0][0].__class__ is cl.StableAgainst + assert inter.relations[1][0].__class__ is cl.NegatedRelation + +def test_obj_on_ceilinglight(): + + bounds = r.constraint_bounds(ex.home_constraints()) + + ceilinglight = r.Domain({t.Semantics.Object, t.Semantics.Lighting, -t.Semantics.Room}, [ + (cu.hanging, r.Domain({t.Semantics.Room, -t.Semantics.Object}, [])) + ]) + + active_bounds = [ + b for b in bounds + if propose_discrete.active_for_stage(ceilinglight, b.domain) + ] + + assert active_bounds == [] + +def test_greedy_partition_home(): + usage_lookup.initialize_from_dict(ex.home_asset_usage()) + prob = ex.home_constraints() + checks.check_problem_greedy_coverage(prob, generate_indoors.default_greedy_stages()) + +def test_contradiction_home(): + prob = ex.home_constraints() + checks.check_contradictory_domains(prob) + +@pytest.mark.parametrize('rtype', sorted(cu.room_types, key=lambda x: x.name)) +def test_room_has_viols_at_init(rtype): + + prob = ex.home_constraints() + + ostate_name = str(rtype) + state = state_def.State({ + ostate_name: state_def.ObjectState( + obj=butil.spawn_cube(), generator=None, tags={rtype, t.Semantics.Room}, relations=[] + ) + }) + + active_count = greedy.update_active_flags(state, {cu.variable_room: ostate_name}) + print("active", rtype, active_count) + assert active_count > 0 + + filter = generate_indoors.default_greedy_stages()['on_floor'] + filter = r.domain_tag_substitute(filter, cu.variable_room, r.Domain({rtype, t.Semantics.Room})) + + result = evaluator.evaluate_problem(prob, state, filter) + + assert result.viol_count() > 0 + + + + + + + + + + \ No newline at end of file From baca32ebb0adc7a57fc136031e159ceebe584460 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 081/727] Add 21 lines to tests/solver/test_greedy_stages.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/solver/test_greedy_stages.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/solver/test_greedy_stages.py b/tests/solver/test_greedy_stages.py index 2ec5ecf4a..4e319384e 100644 --- a/tests/solver/test_greedy_stages.py +++ b/tests/solver/test_greedy_stages.py @@ -30,10 +30,12 @@ from infinigen.core.util import blender as butil @pytest.mark.parametrize('key', generate_indoors.default_greedy_stages().keys()) +def test_stages_relations(key): pprint(generate_indoors.default_greedy_stages()) v = generate_indoors.default_greedy_stages()[key] + assert not v.is_recursive() if len(v.relations) != 0: @@ -42,6 +44,7 @@ raise ValueError(f"Stage {key} has no positive relation, {[r for r, _ in v.relations]}") #if any(isinstance(r, cl.AnyRelation) for r, _ in v.relations): # raise ValueError(f"Stage {key} has an AnyRelation which is underspecified, {v}") + #@pytest.mark.parametrize('key', generate_indoors.default_greedy_stages().keys()) #@pytest.mark.parametrize('roomtype', cu.room_types) #def test_stage_bound_roomsubs(key: str, roomtype: t.Semantics): @@ -81,11 +84,16 @@ def test_validate_stages(): def test_example_intersects(): on_wall_complex = cl.StableAgainst( + {-t.Subpart.Top, t.Subpart.Back, -t.Subpart.Front}, + {t.Subpart.Wall, t.Subpart.Visible, -t.Subpart.Ceiling, -t.Subpart.SupportSurface} ) + on_wall_simple = cl.StableAgainst({}, {t.Subpart.Wall}) assert on_wall_simple.intersects(on_wall_complex) dom = r.Domain( + {t.Semantics.WallDecoration, t.Semantics.Object}, relations=[ + (on_wall_complex, r.Domain({t.Semantics.Room}, [])) ] ) @@ -95,15 +103,28 @@ def test_example_intersects(): def test_contradiction_fail(): prob = cl.Problem(constraints=[ + cl.scene()[{t.Semantics.Object, -t.Semantics.Object}].count().in_range(1, 3) ], score_terms=[]) with pytest.raises(ValueError): checks.check_contradictory_domains(prob) def get_walldec(): return r.Domain( + {t.Semantics.WallDecoration, t.Semantics.Object, -t.Semantics.Room}, + [( + cl.StableAgainst( + {-t.Subpart.Front, -t.Subpart.Top, t.Subpart.Back}, + {-t.Subpart.SupportSurface, -t.Subpart.Ceiling, t.Subpart.Visible, t.Subpart.Wall} + ), + r.Domain({t.Semantics.Room, -t.Semantics.Object}, []) + )] + ) + def test_example_walldec(): dom = get_walldec() + stages = generate_indoors.default_greedy_stages() + assert not propose_discrete.active_for_stage(dom, stages['on_ceiling']) assert not propose_discrete.active_for_stage(dom, stages['on_floor']) From 1973eb3953c12eb61f1de9ef9743a1aabfba45d3 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 082/727] Add 57 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by David Yan. --- tests/assets/test_placeholders.py | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/assets/test_placeholders.py diff --git a/tests/assets/test_placeholders.py b/tests/assets/test_placeholders.py new file mode 100644 index 000000000..b98bcb2bc --- /dev/null +++ b/tests/assets/test_placeholders.py @@ -0,0 +1,57 @@ +# Copyright (c) Princeton University. +# This 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 +import pytest + +from infinigen.core.constraints import ( + usage_lookup, + constraint_language as cl +) + + + +from infinigen.core.util import blender as butil +import numpy as np + + +def get_real_placeholder_facs(): + used_as = home_asset_usage() + usage_lookup.initialize_from_dict(used_as) + +def get_asset_facs(): + used_as = home_asset_usage() + usage_lookup.initialize_from_dict(used_as) + +@pytest.mark.parametrize('fac', get_real_placeholder_facs()) +def test_real_placeholders(fac): + butil.clear_scene() + placeholder = fac(0).spawn_placeholder(0, loc=(0,0,0), rot=(0,0,0)) + asset = fac(0).spawn_asset(0) + assert np.abs(placeholder.dimensions.x - asset.dimensions.x) <= 0.05 * np.abs(asset.dimensions.x), "X dimension of placeholder not within 5 percent of mesh" + assert np.abs(placeholder.dimensions.y - asset.dimensions.y) <= 0.05 * np.abs(asset.dimensions.y), "Y dimension of placeholder not within 5 percent of mesh" + assert np.abs(placeholder.dimensions.z - asset.dimensions.z) <= 0.05 * np.abs(asset.dimensions.z), "Z dimension of placeholder not within 5 percent of mesh" + asset_min_corner = np.array(asset.bound_box[0]) # loXloYloZ https://blender.stackexchange.com/questions/32283/what-are-all-values-in-bound-box + asset_max_corner = np.array(asset.bound_box[6]) # hiXhiYhiZ + ph_min_corner = np.array(placeholder.bound_box[0]) + ph_max_corner = np.array(placeholder.bound_box[6]) + for i in range(3): + assert asset_min_corner[i] <= ph_max_corner[i] and asset_min_corner[i] >= ph_min_corner[i], "Asset not completely contained within placeholder" + assert asset_max_corner[i] <= ph_max_corner[i] and asset_max_corner[i] >= ph_min_corner[i], "Asset not completely contained within placeholder" + + +@pytest.mark.parametrize('fac', get_asset_facs()) +def test_generated_placeholders(fac): + butil.clear_scene() + fac(0).spawn_placeholder(0, loc=(0,0,0), rot=(0,0,0)) + fac(0).spawn_asset(0) + return + + + + + + + From 316f98514b62a4c107247ebff2180cc45f6dc415 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 083/727] Add 7 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/assets/test_placeholders.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/assets/test_placeholders.py b/tests/assets/test_placeholders.py index b98bcb2bc..5a9e9c272 100644 --- a/tests/assets/test_placeholders.py +++ b/tests/assets/test_placeholders.py @@ -3,6 +3,8 @@ # Authors: David Yan +from collections import OrderedDict + import bpy import pytest @@ -11,7 +13,9 @@ constraint_language as cl ) +from infinigen.core.constraints.example_solver.geometry import dof +from infinigen_examples.indoor_asset_semantics import home_asset_usage from infinigen.core.util import blender as butil import numpy as np @@ -20,11 +24,14 @@ def get_real_placeholder_facs(): used_as = home_asset_usage() usage_lookup.initialize_from_dict(used_as) + return sorted(list(pholder_facs), key=lambda x: x.__name__) def get_asset_facs(): used_as = home_asset_usage() usage_lookup.initialize_from_dict(used_as) + return sorted(list(asset_facs), key=lambda x: x.__name__) +@pytest.mark.skip # TODO re-enable. Too many assets fail this @pytest.mark.parametrize('fac', get_real_placeholder_facs()) def test_real_placeholders(fac): butil.clear_scene() From d44ff06bff310d89b9515cccb6a59e3859b5558d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 084/727] Add 4 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/assets/test_placeholders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/assets/test_placeholders.py b/tests/assets/test_placeholders.py index 5a9e9c272..b1b03f3e5 100644 --- a/tests/assets/test_placeholders.py +++ b/tests/assets/test_placeholders.py @@ -18,19 +18,23 @@ from infinigen_examples.indoor_asset_semantics import home_asset_usage from infinigen.core.util import blender as butil +from infinigen.core import tagging, tags as t import numpy as np def get_real_placeholder_facs(): used_as = home_asset_usage() usage_lookup.initialize_from_dict(used_as) + pholder_facs = usage_lookup.factories_for_usage({t.Semantics.RealPlaceholder}) return sorted(list(pholder_facs), key=lambda x: x.__name__) def get_asset_facs(): used_as = home_asset_usage() usage_lookup.initialize_from_dict(used_as) + asset_facs = usage_lookup.factories_for_usage({t.Semantics.PlaceholderBBox}) return sorted(list(asset_facs), key=lambda x: x.__name__) + @pytest.mark.skip # TODO re-enable. Too many assets fail this @pytest.mark.parametrize('fac', get_real_placeholder_facs()) def test_real_placeholders(fac): From 0d32bc6fc65c23f679983655fd3baee248fcc8d8 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 085/727] Add 89 lines to tests/assets/test_meshes_basic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/assets/test_meshes_basic.py | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/assets/test_meshes_basic.py diff --git a/tests/assets/test_meshes_basic.py b/tests/assets/test_meshes_basic.py new file mode 100644 index 000000000..5c77fa38d --- /dev/null +++ b/tests/assets/test_meshes_basic.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: Alexander Raistrick + +from pathlib import Path + +import pytest +import bpy +import gin +from math import prod + +from infinigen.core.util import blender as butil + +from infinigen_examples.util.test_utils import ( + setup_gin, + import_item, + load_txt_list, +) + +def check_factory_runs(fac_class, seed1=0, seed2=0, distance_m=50): + + butil.clear_scene() + fac = fac_class(seed1) + asset = fac.spawn_asset(seed2, distance=distance_m) + + if not isinstance(asset, bpy.types.Object): + raise ValueError(f'{asset.name=} had {type(asset)=}') + + if tuple(asset.location) != (0,0,0): + raise ValueError(f'{asset.location=}') + if tuple(asset.rotation_euler) != (0,0,0): + raise ValueError(f'{asset.rotation_euler=}') + if tuple(asset.scale) != (1,1,1): + raise ValueError(f'{asset.scale=}') + + # currently, assets may have objects as '.children'. + # This will eventually be removed except well-documented special cases + for o in butil.iter_object_tree(asset): + + for i, slot in enumerate(o.material_slots): + if slot.material is None: + raise ValueError(f'In {asset.name=} {slot=} had {slot.material=}') + + for mod in asset.modifiers: + if ( + mod.type != 'NODES' + and mod.type != 'SUBSURF' + ): + # currently we allow unapplied non-modifiers for things like time-based deformation on + # seaweed etc. NODES and SUBSURF should still always be applied. + continue + raise ValueError(f'In {asset.name=} {o.name=} had unapplied modifier {mod.name=} {mod.type=} ') + + if o.type != 'MESH': + continue + + if o.data is None: + raise ValueError(f'In {asset.name=} {o.name=} had {o.data=}') + + if len(o.data.vertices) <= 2: + raise ValueError(f'{asset.name=} had {len(o.data.vertices)} vertices, usually indicates failed operation') + + if tagging.COMBINED_ATTR_NAME in o.data.attributes: + attr = o.data.attributes[tagging.COMBINED_ATTR_NAME] + if attr.domain != 'FACE': + raise ValueError(f'In {asset.name=} had {attr.domain=} for {attr.name=}. Should be FACE') + + # some objects like the older LeafFactory + #if len(o.data.polygons) < 2: + # raise ValueError(f'{asset.name=} had {len(o.data.polygons)} polygons, usually indicates failed operation') + + for attr in o.data.attributes: + if attr.name.startswith(tagging.PREFIX): + raise ValueError(f'In {asset.name}, {o.name=} had un-merged tag-system tag {attr.name=}, need to call {tagging.tag_system.relabel_obj}') + +@pytest.mark.nature +@pytest.mark.parametrize('pathspec', load_txt_list(Path(__file__).parent/'list_nature_meshes.txt')) +def test_nature_factory_runs(pathspec, **kwargs): + setup_gin('infinigen_examples/configs_nature') + fac_class = import_item(pathspec) + check_factory_runs(fac_class, **kwargs) + +@pytest.mark.parametrize('pathspec', load_txt_list(Path(__file__).parent/'list_indoor_meshes.txt')) +def test_indoor_factory_runs(pathspec, **kwargs): + setup_gin('infinigen_examples/configs_indoor') + fac_class = import_item(pathspec) + check_factory_runs(fac_class, **kwargs) From f9d7fbaff6cd3ba0369374663b6a8bf80434356f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 086/727] Add 1 lines to tests/assets/test_meshes_basic.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/assets/test_meshes_basic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/test_meshes_basic.py b/tests/assets/test_meshes_basic.py index 5c77fa38d..f6a3a2e95 100644 --- a/tests/assets/test_meshes_basic.py +++ b/tests/assets/test_meshes_basic.py @@ -12,6 +12,7 @@ from math import prod from infinigen.core.util import blender as butil +from infinigen.core import tagging, tags as t from infinigen_examples.util.test_utils import ( setup_gin, From 447e567b598b473a4cfbada977b922f18f8ffa42 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 087/727] Add 66 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/assets/list_indoor_meshes.txt | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/assets/list_indoor_meshes.txt diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt new file mode 100644 index 000000000..540c867ec --- /dev/null +++ b/tests/assets/list_indoor_meshes.txt @@ -0,0 +1,66 @@ +infinigen.assets.appliances.MicrowaveFactory +infinigen.assets.appliances.MonitorFactory +infinigen.assets.appliances.TVFactory +infinigen.assets.bathroom.BathroomSinkFactory +infinigen.assets.bathroom.BathtubFactory +infinigen.assets.bathroom.HardwareFactory +infinigen.assets.bathroom.ToiletFactory +infinigen.assets.clothes.BlanketFactory +infinigen.assets.clothes.PantsFactory +infinigen.assets.clothes.ShirtFactory +infinigen.assets.decor.AquariumTankFactory +infinigen.assets.elements.doors.GlassPanelDoorFactory +infinigen.assets.elements.doors.LiteDoorFactory +infinigen.assets.elements.doors.LouverDoorFactory +infinigen.assets.elements.doors.PanelDoorFactory +infinigen.assets.elements.staircases.CantileverStaircaseFactory +infinigen.assets.elements.staircases.CurvedStaircaseFactory +infinigen.assets.elements.staircases.LShapedStaircaseFactory +infinigen.assets.elements.staircases.SpiralStaircaseFactory +infinigen.assets.elements.staircases.StraightStaircaseFactory +infinigen.assets.elements.staircases.UShapedStaircaseFactory +infinigen.assets.lighting.CeilingLightFactory +infinigen.assets.lighting.LampFactory +infinigen.assets.lighting.DeskLampFactory +infinigen.assets.lighting.FloorLampFactory +infinigen.assets.seating.BedFactory +infinigen.assets.seating.MattressFactory +infinigen.assets.seating.PillowFactory +infinigen.assets.seating.SofaFactory +infinigen.assets.seating.ArmChairFactory +infinigen.assets.shelves.SingleCabinetFactory +infinigen.assets.shelves.KitchenCabinetFactory +infinigen.assets.shelves.CellShelfFactory +infinigen.assets.shelves.LargeShelfFactory +infinigen.assets.shelves.SimpleBookcaseFactory +infinigen.assets.shelves.SimpleDeskFactory +infinigen.assets.shelves.TriangleShelfFactory +infinigen.assets.shelves.KitchenSpaceFactory +infinigen.assets.shelves.KitchenIslandFactory +infinigen.assets.shelves.TVStandFactory +infinigen.assets.table_decorations.BookColumnFactory +infinigen.assets.table_decorations.BookFactory +infinigen.assets.table_decorations.BookStackFactory +infinigen.assets.table_decorations.SinkFactory +infinigen.assets.table_decorations.TapFactory +infinigen.assets.table_decorations.VaseFactory +infinigen.assets.tables.TableCocktailFactory +infinigen.assets.tables.TableDiningFactory +infinigen.assets.tableware.BottleFactory +infinigen.assets.tableware.BowlFactory +infinigen.assets.tableware.CanFactory +infinigen.assets.tableware.ChopsticksFactory +infinigen.assets.tableware.CupFactory +infinigen.assets.tableware.FoodBagFactory +infinigen.assets.tableware.FoodBoxFactory +infinigen.assets.tableware.ForkFactory +infinigen.assets.tableware.FruitContainerFactory +infinigen.assets.tableware.JarFactory +infinigen.assets.tableware.KnifeFactory +infinigen.assets.tableware.LidFactory +infinigen.assets.tableware.PanFactory +infinigen.assets.tableware.PlateFactory +infinigen.assets.tableware.PotFactory +infinigen.assets.tableware.SpoonFactory +infinigen.assets.tableware.WineglassFactory +infinigen.assets.wall_decorations.BalloonFactory From de9040155d744ebc712b7cbb1ff95a5944af59e1 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 088/727] Add 28 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- tests/assets/list_indoor_meshes.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt index 540c867ec..73279849b 100644 --- a/tests/assets/list_indoor_meshes.txt +++ b/tests/assets/list_indoor_meshes.txt @@ -1,14 +1,21 @@ +infinigen.assets.appliances.BeverageFridgeFactory +infinigen.assets.appliances.DishwasherFactory infinigen.assets.appliances.MicrowaveFactory +infinigen.assets.appliances.OvenFactory infinigen.assets.appliances.MonitorFactory infinigen.assets.appliances.TVFactory + infinigen.assets.bathroom.BathroomSinkFactory infinigen.assets.bathroom.BathtubFactory infinigen.assets.bathroom.HardwareFactory infinigen.assets.bathroom.ToiletFactory + infinigen.assets.clothes.BlanketFactory infinigen.assets.clothes.PantsFactory infinigen.assets.clothes.ShirtFactory +infinigen.assets.clothes.TowelFactory infinigen.assets.decor.AquariumTankFactory + infinigen.assets.elements.doors.GlassPanelDoorFactory infinigen.assets.elements.doors.LiteDoorFactory infinigen.assets.elements.doors.LouverDoorFactory @@ -19,15 +26,26 @@ infinigen.assets.elements.staircases.LShapedStaircaseFactory infinigen.assets.elements.staircases.SpiralStaircaseFactory infinigen.assets.elements.staircases.StraightStaircaseFactory infinigen.assets.elements.staircases.UShapedStaircaseFactory +infinigen.assets.elements.warehouses.RackFactory +infinigen.assets.elements.warehouses.PalletFactory +infinigen.assets.elements.RugFactory +infinigen.assets.elements.NatureShelfTrinketsFactory + infinigen.assets.lighting.CeilingLightFactory infinigen.assets.lighting.LampFactory infinigen.assets.lighting.DeskLampFactory infinigen.assets.lighting.FloorLampFactory +infinigen.assets.lighting.ceiling_classic_lamp.CeilingClassicLampFactory + +infinigen.assets.seating.chairs.BarChairFactory +infinigen.assets.seating.chairs.OfficeChairFactory infinigen.assets.seating.BedFactory +infinigen.assets.seating.BedFrameFactory infinigen.assets.seating.MattressFactory infinigen.assets.seating.PillowFactory infinigen.assets.seating.SofaFactory infinigen.assets.seating.ArmChairFactory + infinigen.assets.shelves.SingleCabinetFactory infinigen.assets.shelves.KitchenCabinetFactory infinigen.assets.shelves.CellShelfFactory @@ -38,14 +56,17 @@ infinigen.assets.shelves.TriangleShelfFactory infinigen.assets.shelves.KitchenSpaceFactory infinigen.assets.shelves.KitchenIslandFactory infinigen.assets.shelves.TVStandFactory + infinigen.assets.table_decorations.BookColumnFactory infinigen.assets.table_decorations.BookFactory infinigen.assets.table_decorations.BookStackFactory infinigen.assets.table_decorations.SinkFactory infinigen.assets.table_decorations.TapFactory infinigen.assets.table_decorations.VaseFactory + infinigen.assets.tables.TableCocktailFactory infinigen.assets.tables.TableDiningFactory + infinigen.assets.tableware.BottleFactory infinigen.assets.tableware.BowlFactory infinigen.assets.tableware.CanFactory @@ -63,4 +84,11 @@ infinigen.assets.tableware.PlateFactory infinigen.assets.tableware.PotFactory infinigen.assets.tableware.SpoonFactory infinigen.assets.tableware.WineglassFactory +infinigen.assets.tableware.PlantContainerFactory +infinigen.assets.tableware.LargePlantContainerFactory + +infinigen.assets.wall_decorations.WallArtFactory infinigen.assets.wall_decorations.BalloonFactory +infinigen.assets.wall_decorations.MirrorFactory + +infinigen.assets.windows.WindowFactory From baad9877171a42c080238dab8ee85833e93d408b Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 089/727] Add 3 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- tests/assets/list_indoor_meshes.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt index 73279849b..7f373d340 100644 --- a/tests/assets/list_indoor_meshes.txt +++ b/tests/assets/list_indoor_meshes.txt @@ -92,3 +92,6 @@ infinigen.assets.wall_decorations.BalloonFactory infinigen.assets.wall_decorations.MirrorFactory infinigen.assets.windows.WindowFactory +infinigen.assets.organizer.basket.BasketBaseFactory + + From 6771a0177913ef742b62269a036fa599d1f8d199 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 090/727] Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- tests/assets/list_indoor_meshes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt index 7f373d340..4a72c9772 100644 --- a/tests/assets/list_indoor_meshes.txt +++ b/tests/assets/list_indoor_meshes.txt @@ -92,6 +92,7 @@ infinigen.assets.wall_decorations.BalloonFactory infinigen.assets.wall_decorations.MirrorFactory infinigen.assets.windows.WindowFactory + infinigen.assets.organizer.basket.BasketBaseFactory From 3772100cb29383e85791d89bcc6d5b6f13cbb022 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 091/727] Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Pvl Bot. --- tests/assets/list_indoor_meshes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt index 4a72c9772..50c41eae3 100644 --- a/tests/assets/list_indoor_meshes.txt +++ b/tests/assets/list_indoor_meshes.txt @@ -38,6 +38,7 @@ infinigen.assets.lighting.FloorLampFactory infinigen.assets.lighting.ceiling_classic_lamp.CeilingClassicLampFactory infinigen.assets.seating.chairs.BarChairFactory +infinigen.assets.seating.chairs.ChairFactory infinigen.assets.seating.chairs.OfficeChairFactory infinigen.assets.seating.BedFactory infinigen.assets.seating.BedFrameFactory From a40916e2ad8fc91dcb5778310c7abd1e11343d07 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 092/727] Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Beining Han. --- tests/assets/list_indoor_meshes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_meshes.txt b/tests/assets/list_indoor_meshes.txt index 50c41eae3..61f4cc886 100644 --- a/tests/assets/list_indoor_meshes.txt +++ b/tests/assets/list_indoor_meshes.txt @@ -95,5 +95,6 @@ infinigen.assets.wall_decorations.MirrorFactory infinigen.assets.windows.WindowFactory infinigen.assets.organizer.basket.BasketBaseFactory +infinigen.assets.organizer.plate_rack.PlateOnRackBaseFactory From ff35ef085d1ba8f743bc62bd36d9e735230b23b8 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 093/727] Add 40 lines to tests/assets/test_materials_basic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/assets/test_materials_basic.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/assets/test_materials_basic.py diff --git a/tests/assets/test_materials_basic.py b/tests/assets/test_materials_basic.py new file mode 100644 index 000000000..61a79145f --- /dev/null +++ b/tests/assets/test_materials_basic.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: Alexander Raistrick + +from pathlib import Path +import importlib + +import pytest +import bpy +import gin + +from infinigen.core.util import blender as butil + +from infinigen_examples.util.test_utils import (setup_gin, load_txt_list, import_item) + +def check_material_runs(pathspec): + butil.clear_scene() + bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=5) + asset = bpy.context.active_object + + mat = import_item(pathspec) + mat.apply(asset) + + # should not crash for input LIST of objects + bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=5) + asset2 = bpy.context.active_object + mat.apply([asset, asset2]) + + + +@pytest.mark.nature +@pytest.mark.parametrize('pathspec', load_txt_list('tests/assets/list_nature_materials.txt')) +def test_nature_material_runs(pathspec, **kwargs): + setup_gin('infinigen_examples/configs_nature') + check_material_runs(pathspec) +@pytest.mark.parametrize('pathspec', load_txt_list('tests/assets/list_indoor_materials.txt')) +def test_indoor_material_runs(pathspec, **kwargs): + setup_gin('infinigen_examples/configs_indoor') From 07fcd9eb97e243c019123b3fab9886fcf15ee85e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 094/727] Add 7 lines to tests/assets/test_materials_basic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- tests/assets/test_materials_basic.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/assets/test_materials_basic.py b/tests/assets/test_materials_basic.py index 61a79145f..ff654c17b 100644 --- a/tests/assets/test_materials_basic.py +++ b/tests/assets/test_materials_basic.py @@ -15,12 +15,15 @@ from infinigen_examples.util.test_utils import (setup_gin, load_txt_list, import_item) + def check_material_runs(pathspec): butil.clear_scene() bpy.ops.mesh.primitive_ico_sphere_add(radius=.8, subdivisions=5) asset = bpy.context.active_object mat = import_item(pathspec) + if type(mat) is type: + mat = mat(0) mat.apply(asset) # should not crash for input LIST of objects @@ -30,11 +33,15 @@ def check_material_runs(pathspec): + @pytest.mark.nature @pytest.mark.parametrize('pathspec', load_txt_list('tests/assets/list_nature_materials.txt')) def test_nature_material_runs(pathspec, **kwargs): setup_gin('infinigen_examples/configs_nature') check_material_runs(pathspec) + + @pytest.mark.parametrize('pathspec', load_txt_list('tests/assets/list_indoor_materials.txt')) def test_indoor_material_runs(pathspec, **kwargs): setup_gin('infinigen_examples/configs_indoor') + check_material_runs(pathspec) From 3bf7a6e0829a659c31926569ef60d23d43c3990b Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 095/727] Add 7 lines to tests/assets/list_displaced_materials.txt. Contributed as part of Infinigen-Indoors by David Yan. --- tests/assets/list_displaced_materials.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/assets/list_displaced_materials.txt diff --git a/tests/assets/list_displaced_materials.txt b/tests/assets/list_displaced_materials.txt new file mode 100644 index 000000000..4444025e9 --- /dev/null +++ b/tests/assets/list_displaced_materials.txt @@ -0,0 +1,7 @@ +infinigen.assets.materials.leather_and_fabrics.fabric +infinigen.assets.materials.leather_and_fabrics.leather +infinigen.assets.materials.metal.grained_and_polished_metal +infinigen.assets.materials.metal.hammered_metal +infinigen.assets.materials.stone_and_concrete.concrete +infinigen.assets.materials.woods.tiled_wood +infinigen.assets.materials.plastics.plastic_rough From 97cd8b47634609e37bf46c49eb40787602eab0db Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 096/727] Add 17 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- tests/assets/list_indoor_materials.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/assets/list_indoor_materials.txt diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt new file mode 100644 index 000000000..0e179a046 --- /dev/null +++ b/tests/assets/list_indoor_materials.txt @@ -0,0 +1,17 @@ +infinigen.assets.materials.fabrics +infinigen.assets.materials.leather +infinigen.assets.materials.sofa_fabric +infinigen.assets.materials.brushed_metal +infinigen.assets.materials.galvanized_metal +infinigen.assets.materials.grained_and_polished_metal +infinigen.assets.materials.hammered_metal +infinigen.assets.materials.metal_basic +infinigen.assets.materials.concrete +infinigen.assets.materials.tiled_wood +infinigen.assets.materials.ArtRug +infinigen.assets.materials.ArtFabric +infinigen.assets.materials.brick +infinigen.assets.materials.ceramic +infinigen.assets.materials.plaster +infinigen.assets.materials.wood_old +infinigen.assets.materials.tiled_wood From bbf50759fbb880b9789746296c12fb6d2198cff6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 097/727] Add 12 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- tests/assets/list_indoor_materials.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt index 0e179a046..a705af7b3 100644 --- a/tests/assets/list_indoor_materials.txt +++ b/tests/assets/list_indoor_materials.txt @@ -8,10 +8,22 @@ infinigen.assets.materials.hammered_metal infinigen.assets.materials.metal_basic infinigen.assets.materials.concrete infinigen.assets.materials.tiled_wood +infinigen.assets.materials.art infinigen.assets.materials.ArtRug infinigen.assets.materials.ArtFabric infinigen.assets.materials.brick infinigen.assets.materials.ceramic +infinigen.assets.materials.glass +infinigen.assets.materials.hardwood_floor +infinigen.assets.materials.leather_and_fabrics.leather +infinigen.assets.materials.marble_voronoi +infinigen.assets.materials.metal.metal_basic +infinigen.assets.materials.mirror infinigen.assets.materials.plaster +infinigen.assets.materials.plastic +infinigen.assets.materials.rug +infinigen.assets.materials.text +infinigen.assets.materials.tile +infinigen.assets.materials.wood infinigen.assets.materials.wood_old infinigen.assets.materials.tiled_wood From 19b6f737603d9f98ec8e24e690ae771d561db3c7 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:12 -0700 Subject: [PATCH 098/727] Add 3 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- tests/assets/list_indoor_materials.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt index a705af7b3..4991cdbb1 100644 --- a/tests/assets/list_indoor_materials.txt +++ b/tests/assets/list_indoor_materials.txt @@ -1,6 +1,9 @@ infinigen.assets.materials.fabrics infinigen.assets.materials.leather infinigen.assets.materials.sofa_fabric +infinigen.assets.materials.coarse_knit_fabric +infinigen.assets.materials.fine_knit_fabric +infinigen.assets.materials.lined_fabric infinigen.assets.materials.brushed_metal infinigen.assets.materials.galvanized_metal infinigen.assets.materials.grained_and_polished_metal From 2cc2de323baa51062d686d7fb2431277b07f8c0f Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 099/727] Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- tests/assets/list_indoor_materials.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt index 4991cdbb1..6900ee93b 100644 --- a/tests/assets/list_indoor_materials.txt +++ b/tests/assets/list_indoor_materials.txt @@ -19,6 +19,7 @@ infinigen.assets.materials.ceramic infinigen.assets.materials.glass infinigen.assets.materials.hardwood_floor infinigen.assets.materials.leather_and_fabrics.leather +infinigen.assets.materials.leather_and_fabrics.velvet infinigen.assets.materials.marble_voronoi infinigen.assets.materials.metal.metal_basic infinigen.assets.materials.mirror From d20a26b6c24792c49e0990c509fc230b98617880 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 100/727] Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- tests/assets/list_indoor_materials.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt index 6900ee93b..3192194c4 100644 --- a/tests/assets/list_indoor_materials.txt +++ b/tests/assets/list_indoor_materials.txt @@ -31,3 +31,4 @@ infinigen.assets.materials.tile infinigen.assets.materials.wood infinigen.assets.materials.wood_old infinigen.assets.materials.tiled_wood +infinigen.assets.materials.bumpy_rubber_floor From d3a5e35f868d6c3327ef9a9628098033d1f6aabd Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 101/727] Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- tests/assets/list_indoor_materials.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/assets/list_indoor_materials.txt b/tests/assets/list_indoor_materials.txt index 3192194c4..49875dc23 100644 --- a/tests/assets/list_indoor_materials.txt +++ b/tests/assets/list_indoor_materials.txt @@ -20,6 +20,7 @@ infinigen.assets.materials.glass infinigen.assets.materials.hardwood_floor infinigen.assets.materials.leather_and_fabrics.leather infinigen.assets.materials.leather_and_fabrics.velvet +infinigen.assets.materials.marble_regular infinigen.assets.materials.marble_voronoi infinigen.assets.materials.metal.metal_basic infinigen.assets.materials.mirror From 335b2ee226f2d4133328a320af70290338e69144 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 102/727] Add 32 lines to docs/ExportingToSimulators.md. Contributed as part of Infinigen-Indoors by David Yan. --- docs/ExportingToSimulators.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/ExportingToSimulators.md diff --git a/docs/ExportingToSimulators.md b/docs/ExportingToSimulators.md new file mode 100644 index 000000000..89ed6e383 --- /dev/null +++ b/docs/ExportingToSimulators.md @@ -0,0 +1,32 @@ +## 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 +``` + +```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. + + From c552fc07953d7e9701543791c195c5298b3d71ac Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 103/727] Add 1 lines to docs/ExportingToSimulators.md. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- docs/ExportingToSimulators.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ExportingToSimulators.md b/docs/ExportingToSimulators.md index 89ed6e383..fe8fdadf0 100644 --- a/docs/ExportingToSimulators.md +++ b/docs/ExportingToSimulators.md @@ -7,6 +7,7 @@ This documentation details how to run a robotics simulation in an exported Infin 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 From 8f526e03879051c5d01cd7cef0964dcec137b645 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 104/727] Add 125 lines to docs/HelloRoom.md. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- docs/HelloRoom.md | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/HelloRoom.md diff --git a/docs/HelloRoom.md b/docs/HelloRoom.md new file mode 100644 index 000000000..e3dc76328 --- /dev/null +++ b/docs/HelloRoom.md @@ -0,0 +1,125 @@ +# 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 From 8114cfe9817adf7cac2307088e9c05de5a6baed1 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 105/727] Add 2 lines to docs/HelloRoom.md. Contributed as part of Infinigen-Indoors by Pvl Bot. --- docs/HelloRoom.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/HelloRoom.md b/docs/HelloRoom.md index e3dc76328..c35e23611 100644 --- a/docs/HelloRoom.md +++ b/docs/HelloRoom.md @@ -3,6 +3,8 @@

+ +

## Generate a scene step-by-step From 694c98043b37d6d92ed32400ab919d0c5be9e1bb Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 106/727] Add 36 lines to scripts/rebuttal_retry_render.sh. Contributed as part of Infinigen-Indoors by Pvl Bot. --- scripts/rebuttal_retry_render.sh | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 scripts/rebuttal_retry_render.sh diff --git a/scripts/rebuttal_retry_render.sh b/scripts/rebuttal_retry_render.sh new file mode 100644 index 000000000..25d4fcce5 --- /dev/null +++ b/scripts/rebuttal_retry_render.sh @@ -0,0 +1,36 @@ +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/kitchen_0 --output_folder outputs/rebuttal_figure/kitchen_0/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/kitchen_1 --output_folder outputs/rebuttal_figure/kitchen_1/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/kitchen_2 --output_folder outputs/rebuttal_figure/kitchen_2/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/kitchen_3 --output_folder outputs/rebuttal_figure/kitchen_3/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/kitchen_4 --output_folder outputs/rebuttal_figure/kitchen_4/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/kitchen_5 --output_folder outputs/rebuttal_figure/kitchen_5/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/kitchen_6 --output_folder outputs/rebuttal_figure/kitchen_6/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/kitchen_7 --output_folder outputs/rebuttal_figure/kitchen_7/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/kitchen_8 --output_folder outputs/rebuttal_figure/kitchen_8/frames -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/bathroom_0 --output_folder outputs/rebuttal_figure/bathroom_0/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/bathroom_1 --output_folder outputs/rebuttal_figure/bathroom_1/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/bathroom_2 --output_folder outputs/rebuttal_figure/bathroom_2/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/bathroom_3 --output_folder outputs/rebuttal_figure/bathroom_3/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/bathroom_4 --output_folder outputs/rebuttal_figure/bathroom_4/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/bathroom_5 --output_folder outputs/rebuttal_figure/bathroom_5/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/bathroom_6 --output_folder outputs/rebuttal_figure/bathroom_6/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/bathroom_7 --output_folder outputs/rebuttal_figure/bathroom_7/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/bathroom_8 --output_folder outputs/rebuttal_figure/bathroom_8/frames -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/dining_0 --output_folder outputs/rebuttal_figure/dining_0/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/dining_1 --output_folder outputs/rebuttal_figure/dining_1/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/dining_2 --output_folder outputs/rebuttal_figure/dining_2/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/dining_3 --output_folder outputs/rebuttal_figure/dining_3/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/dining_4 --output_folder outputs/rebuttal_figure/dining_4/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/dining_5 --output_folder outputs/rebuttal_figure/dining_5/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/dining_6 --output_folder outputs/rebuttal_figure/dining_6/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/dining_7 --output_folder outputs/rebuttal_figure/dining_7/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/dining_8 --output_folder outputs/rebuttal_figure/dining_8/frames -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/living_0 --output_folder outputs/rebuttal_figure/living_0/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/living_1 --output_folder outputs/rebuttal_figure/living_1/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/living_2 --output_folder outputs/rebuttal_figure/living_2/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/living_3 --output_folder outputs/rebuttal_figure/living_3/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_4/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/living_5 --output_folder outputs/rebuttal_figure/living_5/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/living_6 --output_folder outputs/rebuttal_figure/living_6/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/living_7 --output_folder outputs/rebuttal_figure/living_7/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/living_8 --output_folder outputs/rebuttal_figure/living_8/frames -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom \ No newline at end of file From 8ec2d5cf38d15ede8b2b17a2470e5bb442fe148f Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 107/727] Add 8 lines to scripts/indoor.sh. Contributed as part of Infinigen-Indoors by Beining Han. --- scripts/indoor.sh | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 scripts/indoor.sh diff --git a/scripts/indoor.sh b/scripts/indoor.sh new file mode 100644 index 000000000..b8223affd --- /dev/null +++ b/scripts/indoor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + + +for s in $(seq 0 20) +do + python -m infinigen_examples.generate_indoors --output_folder outputs/room_${s} -s ${s} -g base disable/no_objects -t coarse + python -m infinigen_examples.generate_indoors --input_folder outputs/room_${s} --output_folder outputs/room_${s}/frames -s ${s} -g base disable/no_objects -t render +done From 2e91ff61bbbdeac22341980ab9ae404bec8b0543 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 108/727] Add 72 lines to scripts/rebuttal.sh. Contributed as part of Infinigen-Indoors by Pvl Bot. --- scripts/rebuttal.sh | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts/rebuttal.sh diff --git a/scripts/rebuttal.sh b/scripts/rebuttal.sh new file mode 100644 index 000000000..d3f14d423 --- /dev/null +++ b/scripts/rebuttal.sh @@ -0,0 +1,72 @@ +python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/kitchen_0 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/kitchen_1 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/kitchen_2 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 3 --task coarse --output_folder outputs/rebuttal_figure/kitchen_3 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 4 --task coarse --output_folder outputs/rebuttal_figure/kitchen_4 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_folder outputs/rebuttal_figure/kitchen_5 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/kitchen_6 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/kitchen_7 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/kitchen_8 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/bathroom_0 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/bathroom_1 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/bathroom_2 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 3 --task coarse --output_folder outputs/rebuttal_figure/bathroom_3 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 4 --task coarse --output_folder outputs/rebuttal_figure/bathroom_4 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_folder outputs/rebuttal_figure/bathroom_5 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/bathroom_6 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/bathroom_7 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/bathroom_8 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/dining_0 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/dining_1 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/dining_2 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 3 --task coarse --output_folder outputs/rebuttal_figure/dining_3 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 4 --task coarse --output_folder outputs/rebuttal_figure/dining_4 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_folder outputs/rebuttal_figure/dining_5 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/dining_6 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/dining_7 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/dining_8 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/living_0 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_0/logs.txt & +python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/living_1 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_1/logs.txt & +python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/living_2 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_2/logs.txt & +python -m infinigen_examples.generate_indoors --seed 3 --task coarse --output_folder outputs/rebuttal_figure/living_3 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_3/logs.txt & +python -m infinigen_examples.generate_indoors --seed 4 --task coarse --output_folder outputs/rebuttal_figure/living_4 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_4/logs.txt & +python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_folder outputs/rebuttal_figure/living_5 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_5/logs.txt & +python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/living_6 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_6/logs.txt & +python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/living_7 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_7/logs.txt & +python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/living_8 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_8/logs.txt & +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/kitchen_0 --output_folder outputs/rebuttal_figure/kitchen_0 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/kitchen_1 --output_folder outputs/rebuttal_figure/kitchen_1 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/kitchen_2 --output_folder outputs/rebuttal_figure/kitchen_2 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/kitchen_3 --output_folder outputs/rebuttal_figure/kitchen_3 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/kitchen_4 --output_folder outputs/rebuttal_figure/kitchen_4 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/kitchen_5 --output_folder outputs/rebuttal_figure/kitchen_5 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/kitchen_6 --output_folder outputs/rebuttal_figure/kitchen_6 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/kitchen_7 --output_folder outputs/rebuttal_figure/kitchen_7 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/kitchen_8 --output_folder outputs/rebuttal_figure/kitchen_8 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/bathroom_0 --output_folder outputs/rebuttal_figure/bathroom_0 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/bathroom_1 --output_folder outputs/rebuttal_figure/bathroom_1 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/bathroom_2 --output_folder outputs/rebuttal_figure/bathroom_2 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/bathroom_3 --output_folder outputs/rebuttal_figure/bathroom_3 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/bathroom_4 --output_folder outputs/rebuttal_figure/bathroom_4 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/bathroom_5 --output_folder outputs/rebuttal_figure/bathroom_5 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/bathroom_6 --output_folder outputs/rebuttal_figure/bathroom_6 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/bathroom_7 --output_folder outputs/rebuttal_figure/bathroom_7 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/bathroom_8 --output_folder outputs/rebuttal_figure/bathroom_8 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/dining_0 --output_folder outputs/rebuttal_figure/dining_0 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/dining_1 --output_folder outputs/rebuttal_figure/dining_1 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/dining_2 --output_folder outputs/rebuttal_figure/dining_2 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/dining_3 --output_folder outputs/rebuttal_figure/dining_3 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/dining_4 --output_folder outputs/rebuttal_figure/dining_4 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 5 --task render --input_folder outputs/rebuttal_figure/dining_5 --output_folder outputs/rebuttal_figure/dining_5 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/dining_6 --output_folder outputs/rebuttal_figure/dining_6 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/dining_7 --output_folder outputs/rebuttal_figure/dining_7 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/dining_8 --output_folder outputs/rebuttal_figure/dining_8 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/living_0 --output_folder outputs/rebuttal_figure/living_0 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/living_1 --output_folder outputs/rebuttal_figure/living_1 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/living_2 --output_folder outputs/rebuttal_figure/living_2 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & +python -m infinigen_examples.generate_indoors --seed 3 --task render --input_folder outputs/rebuttal_figure/living_3 --output_folder outputs/rebuttal_figure/living_3 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_4 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_5 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_6 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_7 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom +python -m infinigen_examples.generate_indoors --seed 4 --task render --input_folder outputs/rebuttal_figure/living_4 --output_folder outputs/rebuttal_figure/living_8 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom \ No newline at end of file From fda689a1659ce76046079ce442600bc0982420e7 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 109/727] Add 11 lines to scripts/rebuttal.sh. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- scripts/rebuttal.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/rebuttal.sh b/scripts/rebuttal.sh index d3f14d423..47511eacf 100644 --- a/scripts/rebuttal.sh +++ b/scripts/rebuttal.sh @@ -7,6 +7,7 @@ python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_fo python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/kitchen_6 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/kitchen_7 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/kitchen_8 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom & + python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/bathroom_0 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/bathroom_1 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/bathroom_2 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & @@ -16,6 +17,9 @@ python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_fo python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/bathroom_6 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/bathroom_7 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/bathroom_8 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom & + +wait $(jobs -ps) + python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/dining_0 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/dining_1 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/dining_2 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & @@ -25,6 +29,7 @@ python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_fo python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/dining_6 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/dining_7 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/dining_8 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & + python -m infinigen_examples.generate_indoors --seed 0 --task coarse --output_folder outputs/rebuttal_figure/living_0 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_0/logs.txt & python -m infinigen_examples.generate_indoors --seed 1 --task coarse --output_folder outputs/rebuttal_figure/living_1 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_1/logs.txt & python -m infinigen_examples.generate_indoors --seed 2 --task coarse --output_folder outputs/rebuttal_figure/living_2 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_2/logs.txt & @@ -34,6 +39,9 @@ python -m infinigen_examples.generate_indoors --seed 5 --task coarse --output_fo python -m infinigen_examples.generate_indoors --seed 6 --task coarse --output_folder outputs/rebuttal_figure/living_6 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_6/logs.txt & python -m infinigen_examples.generate_indoors --seed 7 --task coarse --output_folder outputs/rebuttal_figure/living_7 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_7/logs.txt & python -m infinigen_examples.generate_indoors --seed 8 --task coarse --output_folder outputs/rebuttal_figure/living_8 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom > outputs/rebuttal_figure/living_8/logs.txt & + +wait $(jobs -ps) + python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/kitchen_0 --output_folder outputs/rebuttal_figure/kitchen_0 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/kitchen_1 --output_folder outputs/rebuttal_figure/kitchen_1 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/kitchen_2 --output_folder outputs/rebuttal_figure/kitchen_2 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom @@ -43,6 +51,7 @@ python -m infinigen_examples.generate_indoors --seed 5 --task render --input_fol python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/kitchen_6 --output_folder outputs/rebuttal_figure/kitchen_6 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/kitchen_7 --output_folder outputs/rebuttal_figure/kitchen_7 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/kitchen_8 --output_folder outputs/rebuttal_figure/kitchen_8 -p compose_indoors.room_tags=[\"kitchen\"] --configs overhead_singleroom + python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/bathroom_0 --output_folder outputs/rebuttal_figure/bathroom_0 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/bathroom_1 --output_folder outputs/rebuttal_figure/bathroom_1 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/bathroom_2 --output_folder outputs/rebuttal_figure/bathroom_2 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom @@ -52,6 +61,7 @@ python -m infinigen_examples.generate_indoors --seed 5 --task render --input_fol python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/bathroom_6 --output_folder outputs/rebuttal_figure/bathroom_6 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/bathroom_7 --output_folder outputs/rebuttal_figure/bathroom_7 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/bathroom_8 --output_folder outputs/rebuttal_figure/bathroom_8 -p compose_indoors.room_tags=[\"bathroom\"] --configs overhead_singleroom + python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/dining_0 --output_folder outputs/rebuttal_figure/dining_0 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/dining_1 --output_folder outputs/rebuttal_figure/dining_1 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/dining_2 --output_folder outputs/rebuttal_figure/dining_2 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom & @@ -61,6 +71,7 @@ python -m infinigen_examples.generate_indoors --seed 5 --task render --input_fol python -m infinigen_examples.generate_indoors --seed 6 --task render --input_folder outputs/rebuttal_figure/dining_6 --output_folder outputs/rebuttal_figure/dining_6 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 7 --task render --input_folder outputs/rebuttal_figure/dining_7 --output_folder outputs/rebuttal_figure/dining_7 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom python -m infinigen_examples.generate_indoors --seed 8 --task render --input_folder outputs/rebuttal_figure/dining_8 --output_folder outputs/rebuttal_figure/dining_8 -p compose_indoors.room_tags=[\"dining-room\"] --configs overhead_singleroom + python -m infinigen_examples.generate_indoors --seed 0 --task render --input_folder outputs/rebuttal_figure/living_0 --output_folder outputs/rebuttal_figure/living_0 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 1 --task render --input_folder outputs/rebuttal_figure/living_1 --output_folder outputs/rebuttal_figure/living_1 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & python -m infinigen_examples.generate_indoors --seed 2 --task render --input_folder outputs/rebuttal_figure/living_2 --output_folder outputs/rebuttal_figure/living_2 -p compose_indoors.room_tags=[\"living-room\"] --configs overhead_singleroom & From 6e11047b165c6286a43fe963d74d39824a49df9e Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 110/727] Add 63 lines to scripts/eevee_render.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- scripts/eevee_render.py | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 scripts/eevee_render.py diff --git a/scripts/eevee_render.py b/scripts/eevee_render.py new file mode 100644 index 000000000..3a75e0345 --- /dev/null +++ b/scripts/eevee_render.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 argparse +from pathlib import Path +from infinigen.core.util import blender as butil +from infinigen.core.rendering.render import enable_gpu +import mathutils +import bpy + +def get_override(area_type, region_type): + for area in bpy.context.screen.areas: + if area.type == area_type: + for region in area.regions: + if region.type == region_type: + override = {'area': area, 'region': region} + return override + #error message if the area or region wasn't found + raise RuntimeError("Wasn't able to find", region_type," in area ", area_type, + "\n Make sure it's open while executing script.") + + + +def process(scene_folder: Path): + butil.clear_scene() + bpy.ops.wm.open_mainfile(filepath=str(scene_folder/'scene.blend')) + + for o in butil.get_collection('ceiling').objects: + o.active_material.use_backface_culling = True + for o in butil.get_collection('wall').objects: + o.active_material.use_backface_culling = True + + bpy.ops.object.light_add(type='SUN') + light = bpy.context.active_object + light.rotation_euler = (-0.7, 0.1, 0.22) + light.data.energy = 5 + + room = next(o for o in butil.get_collection('floor').objects if not o.hide_render) + + cam = bpy.context.scene.camera + t = mathutils.Matrix.Translation(room.location) + s = mathutils.Matrix.Scale(1.5, 4) + cam.matrix_world = t @ s @ mathutils.Euler((0.42, 0, 0.2)).to_matrix().to_4x4() @ t.inverted() @ cam.matrix_world + + bpy.context.scene.render.filepath = str(scene_folder/'Image_EEVEE') + enable_gpu() + bpy.context.scene.render.engine = 'BLENDER_EEVEE' + bpy.ops.render.render(write_still=True) + + butil.save_blend(scene_folder/'eevee.blend') + +parser = argparse.ArgumentParser() +parser.add_argument('input_folder', type=Path) +args = parser.parse_args() + +for p in args.input_folder.iterdir(): + if not (p/'scene.blend').exists(): + print(f'{p=} has no scene.blend') + continue + process(p) \ No newline at end of file From a76ee4ac7871ca1ddc791f85ec343a0df81d3f0e Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 111/727] Add 83 lines to infinigen/tools/indoor_profile.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/tools/indoor_profile.py | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 infinigen/tools/indoor_profile.py 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) From bf486cbfa78ee0c288a113f43367876b95a84958 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 112/727] Add 139 lines to infinigen/tools/isaac_sim.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/tools/isaac_sim.py | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 infinigen/tools/isaac_sim.py diff --git a/infinigen/tools/isaac_sim.py b/infinigen/tools/isaac_sim.py new file mode 100644 index 000000000..84f6c3b15 --- /dev/null +++ b/infinigen/tools/isaac_sim.py @@ -0,0 +1,139 @@ +# Copyright (c) Princeton University. +# This 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]) + + 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_lighting() + self._add_robot() + + create_prim(prim_path="/World/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() + self.robot = self.scene.add( + WheeledRobot( + name="Robot", + wheel_dof_names=["left_wheel_joint", "right_wheel_joint"], + create_robot=True, + usd_path=robot_path, + ) + ) + self.controller = RobotController() + self.robot.apply_action(self.controller.forward()) + + self.world.reset() + self.world.step(render=True) + + parser.add_argument('--scene-path', type=str) + parser.add_argument('--json-path', type=str) + + From 9e8b743f4959e8aaf4d85a13ffe32131f156b9d7 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 113/727] Add 32 lines to infinigen/tools/isaac_sim.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/tools/isaac_sim.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/infinigen/tools/isaac_sim.py b/infinigen/tools/isaac_sim.py index 84f6c3b15..0f704b2a1 100644 --- a/infinigen/tools/isaac_sim.py +++ b/infinigen/tools/isaac_sim.py @@ -10,6 +10,7 @@ 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 @@ -33,16 +34,25 @@ def __init__(self): 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() @@ -119,21 +129,43 @@ def _get_camera_loc(self): 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() From 1b7d7bdcafe7f8cdda9ac5f37bdda2b7fda3987f Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 114/727] Add 124 lines to infinigen/tools/convert_displacement.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/tools/convert_displacement.py | 124 ++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 infinigen/tools/convert_displacement.py 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"]) + + + + + + + From c4c50b6c088fbfb42534c9556b7ba70ad4aaf623 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 115/727] Add 187 lines to infinigen/tools/perceptual/create_pairs.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/tools/perceptual/create_pairs.py | 187 +++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 infinigen/tools/perceptual/create_pairs.py 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') From 2a7e3c06063e2b79edcb20b0a814d7973b4208ba Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 116/727] Add 13 lines to infinigen/tools/perceptual/rename.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/tools/perceptual/rename.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 infinigen/tools/perceptual/rename.py diff --git a/infinigen/tools/perceptual/rename.py b/infinigen/tools/perceptual/rename.py new file mode 100644 index 000000000..8eb130e21 --- /dev/null +++ b/infinigen/tools/perceptual/rename.py @@ -0,0 +1,13 @@ +# Copyright (c) Princeton University. +# This 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__': + + + From 0448bb68b18df6a0b26f6e1675c36b5693725088 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 117/727] Add 12 lines to infinigen/tools/perceptual/rename.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/tools/perceptual/rename.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/tools/perceptual/rename.py b/infinigen/tools/perceptual/rename.py index 8eb130e21..8f51cd65a 100644 --- a/infinigen/tools/perceptual/rename.py +++ b/infinigen/tools/perceptual/rename.py @@ -8,6 +8,18 @@ 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 From 0779236e8530ed59ecf5bd3f9f7188c8de58f4f5 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 118/727] Add 34 lines to infinigen/tools/perceptual/perceptual_extract.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../tools/perceptual/perceptual_extract.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 infinigen/tools/perceptual/perceptual_extract.py 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 From bdb107e18116f7257c48870b9a51db2cd1e3132b Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 119/727] Add 411 lines to infinigen/tools/perceptual/analysis.ipynb. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/tools/perceptual/analysis.ipynb | 411 ++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 infinigen/tools/perceptual/analysis.ipynb 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 +} From 3ea135237c23de3635df7d411896a226f17cb32a Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 120/727] Add 37 lines to infinigen/tools/perceptual/create_submission.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../tools/perceptual/create_submission.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 infinigen/tools/perceptual/create_submission.py 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) From 3101627efad986549be6129208667a2784caed7b Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 121/727] Add 12 lines to infinigen/tools/config/demo_config.yaml. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/tools/config/demo_config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infinigen/tools/config/demo_config.yaml 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] + From 7c2df1516e1e9386e1a4599d9536f0e7dccd9fdd Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 122/727] Add 70 lines to infinigen/tools/results/visualize_planar_graph.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../tools/results/visualize_planar_graph.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 infinigen/tools/results/visualize_planar_graph.py diff --git a/infinigen/tools/results/visualize_planar_graph.py b/infinigen/tools/results/visualize_planar_graph.py new file mode 100644 index 000000000..586cd1e66 --- /dev/null +++ b/infinigen/tools/results/visualize_planar_graph.py @@ -0,0 +1,70 @@ +# Copyright (c) Princeton University. +# This 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 + +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) From 2cb43030fc9c238dd8b7a0c38d97d0031321fbad Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 123/727] Add 1 lines to infinigen/tools/results/visualize_planar_graph.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/tools/results/visualize_planar_graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/tools/results/visualize_planar_graph.py b/infinigen/tools/results/visualize_planar_graph.py index 586cd1e66..b88480b8b 100644 --- a/infinigen/tools/results/visualize_planar_graph.py +++ b/infinigen/tools/results/visualize_planar_graph.py @@ -16,6 +16,7 @@ # 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): From f1777b7fb08118c16ea20e5db050061c307ac33f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 124/727] Add 215 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/tags.py | 215 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 infinigen/core/tags.py diff --git a/infinigen/core/tags.py b/infinigen/core/tags.py new file mode 100644 index 000000000..4c161719d --- /dev/null +++ b/infinigen/core/tags.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: Alexander Raistrick + +from dataclasses import dataclass + + # 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" + + 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" + + # 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' + + def __str__(self): + return f'{self.__class__.__name__}({self.value})' + + def __repr__(self): + return f'{self.__class__.__name__}.{self.name}' + + 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}' + + def __repr__(self): + return f'{self.__class__.__name__}({self.generator.__name__})' + +@dataclass(frozen=True) +class Negated(Tag): + tag: 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" + + + 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 + + + + positive, negative = set(), set() + + for t in tags: + match t: + case Negated(tag): + negative.add(tag) + case _: + positive.add(t) + return positive, negative + pos, neg = decompose_tags(tags) + 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: + p1, n1 = decompose_tags(t1) + p2, n2 = decompose_tags(t2) + 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 + + fac = next((f for f in fac_context.keys() if f.__name__ == s), None) + if 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 + 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 From f520889f6b642d7659c25aeb6b3cb9ca5ca6d842 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 125/727] Add 81 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/tags.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/infinigen/core/tags.py b/infinigen/core/tags.py index 4c161719d..666fd90d8 100644 --- a/infinigen/core/tags.py +++ b/infinigen/core/tags.py @@ -4,8 +4,29 @@ # 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" @@ -81,6 +102,7 @@ SingleGenerator = 'single-generator' NoRotation = 'no-rotation' NoCollision = 'no-collision' + NoChildren = 'no-children' def __str__(self): return f'{self.__class__.__name__}({self.value})' @@ -88,6 +110,20 @@ def __str__(self): 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): @@ -96,6 +132,10 @@ def __str__(self): 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__})' @@ -112,6 +152,9 @@ def __neg__(self): 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) @@ -122,7 +165,11 @@ def __repr__(self): def __str__(self): return self.name +@dataclass(frozen=True) +class SpecificObject(Tag): + name: str +def decompose_tags(tags: set[Tag]): positive, negative = set(), set() @@ -132,16 +179,34 @@ def __str__(self): 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) @@ -181,9 +246,14 @@ def to_tag(s: str | Tag | type, fac_context=None) -> Tag: 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("\"\'") @@ -203,6 +273,17 @@ 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): From d78a1f252f7ed3cd49e4e265cbc57a132a7c8629 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:13 -0700 Subject: [PATCH 126/727] Add 7 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/core/tags.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/tags.py b/infinigen/core/tags.py index 666fd90d8..f82a1202f 100644 --- a/infinigen/core/tags.py +++ b/infinigen/core/tags.py @@ -27,6 +27,7 @@ class EnumTag(Tag, Enum, metaclass=ABCEnumMeta): pass class Semantics(EnumTag): + # Mesh types Room = "room" Object = "object" @@ -79,6 +80,7 @@ class Semantics(EnumTag): Utensils = "utensils" ClothDrapeItem = "cloth-drape" + # Object Access Type AccessTop = "access-top" AccessFront = "access-front" AccessAnySide = "access-any-side" @@ -92,6 +94,11 @@ class Semantics(EnumTag): # 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 From b3001727385454a85d2764297678476ff3eeceda Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 127/727] Add 3 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/core/tags.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/tags.py b/infinigen/core/tags.py index f82a1202f..f31d38f6d 100644 --- a/infinigen/core/tags.py +++ b/infinigen/core/tags.py @@ -150,6 +150,9 @@ def __repr__(self): class Negated(Tag): tag: Tag + def __str__(self): + return "-" + str(self.tag) + def __repr__(self): return f"-{repr(self.tag)}" From a7d50a160e9f5b8001b0aa7e80923edeae1baf8a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 128/727] Add 345 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/tagging.py | 345 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 infinigen/core/tagging.py diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py new file mode 100644 index 000000000..799a75b81 --- /dev/null +++ b/infinigen/core/tagging.py @@ -0,0 +1,345 @@ +import logging + +from infinigen.core import surface + +logger = logging.getLogger(__name__) + +PREFIX = 'TAG_' +COMBINED_ATTR_NAME = 'MaskTag' + def clear(self): + self.tag_dict = {} + + 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) + + + 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): + self._relabel_obj_single(obj, tag_name_lookup) + +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): + + + 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_TAG_MEANINGS = { +} +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') + + 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' + } + ) + +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 + + + 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: + except ValueError: + return x + return {try_convert(x) for x in res} + + + # 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: + 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 nonempty: + raise ValueError(f'extract_mask({obj.name=}) got empty mask') + 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 From 92856299ff1db247f8b9d67640bb8f4398159c83 Mon Sep 17 00:00:00 2001 From: Lahav Lipson Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 129/727] Add 30 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Lahav Lipson. --- infinigen/core/tagging.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py index 799a75b81..7490360b4 100644 --- a/infinigen/core/tagging.py +++ b/infinigen/core/tagging.py @@ -1,14 +1,33 @@ +import os +import bpy +import json import logging +import numpy as np from infinigen.core import surface 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 = [ @@ -115,6 +134,8 @@ def _relabel_obj_single(self, obj, tag_name_lookup): 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(): @@ -126,7 +147,13 @@ def _relabel_obj_single(self, obj, tag_name_lookup): 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): @@ -138,6 +165,7 @@ def print_segments_summary(obj: bpy.types.Object): 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) @@ -174,6 +202,7 @@ def tag_object(obj, name=None, mask=None): ) 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) @@ -231,6 +260,7 @@ def tag_canonical_surfaces(obj, rtol=0.01): 'data_type': 'BOOLEAN' } ) + return store_named_attribute def _name_for_tagval(i: int) -> str | None: From 80a51b7efcab2d22f3af4b39147496510ad8b8e8 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 130/727] Add 25 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/tagging.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py index 7490360b4..a6ee8b972 100644 --- a/infinigen/core/tagging.py +++ b/infinigen/core/tagging.py @@ -1,11 +1,19 @@ +# Copyright (c) Princeton University. + + + 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 + logger = logging.getLogger(__name__) PREFIX = 'TAG_' @@ -173,6 +181,8 @@ def print_segments_summary(obj: bpy.types.Object): 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): @@ -220,7 +230,12 @@ def vert_mask_to_tri_mask(obj, vert_mask, require_all=True): 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): @@ -245,7 +260,9 @@ def tag_canonical_surfaces(obj, rtol=0.01): 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') +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, @@ -291,10 +308,12 @@ def _name_for_tagval(i: int) -> str | None: 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 @@ -329,8 +348,10 @@ def try_convert(x): return face_mask def extract_tagged_faces(obj: bpy.types.Object, tags: set, nonempty=False) -> bpy.types.Object: + 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( @@ -338,8 +359,12 @@ def extract_mask( 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 From 0a64e81b356c3ae394e531c99769a4e9727ae9ab Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 131/727] Add 19 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/tagging.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py index a6ee8b972..40ab494d6 100644 --- a/infinigen/core/tagging.py +++ b/infinigen/core/tagging.py @@ -1,5 +1,6 @@ # Copyright (c) Princeton University. +# Authors: Yihan Wang, Karhan Kayan: face based tagging, canonical surface tagging, mask extraction import os @@ -14,6 +15,8 @@ from . import tags as t +from typing import Union, Any + logger = logging.getLogger(__name__) PREFIX = 'TAG_' @@ -214,6 +217,7 @@ def tag_object(obj, name=None, mask=None): 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) @@ -230,6 +234,7 @@ def vert_mask_to_tri_mask(obj, vert_mask, require_all=True): 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), @@ -237,14 +242,18 @@ def vert_mask_to_tri_mask(obj, vert_mask, require_all=True): 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] @@ -260,6 +269,9 @@ def tag_canonical_surfaces(obj, rtol=0.01): 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) @@ -347,8 +359,15 @@ def tagged_face_mask(obj: bpy.types.Object, tags: Union[t.Subpart]) -> np.ndarra 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)}') From d93796685b5eeaab72869289465392e55ffc9313 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 132/727] Add 9 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/core/tagging.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/core/tagging.py b/infinigen/core/tagging.py index 40ab494d6..1ec08b5cd 100644 --- a/infinigen/core/tagging.py +++ b/infinigen/core/tagging.py @@ -1,4 +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: Yihan Wang, Karhan Kayan: face based tagging, canonical surface tagging, mask extraction @@ -22,10 +24,13 @@ PREFIX = 'TAG_' COMBINED_ATTR_NAME = 'MaskTag' + class AutoTag(): tag_dict = {} + def __init__(self): self.tag_dict = {} + def clear(self): self.tag_dict = {} @@ -35,6 +40,7 @@ def clear(self): 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) @@ -307,6 +313,7 @@ def _name_for_tagval(i: int) -> str | None: return name +def union_object_tags(obj): if COMBINED_ATTR_NAME not in obj.data.attributes: return set() @@ -323,6 +330,7 @@ def try_convert(x): 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: @@ -366,6 +374,7 @@ def extract_tagged_faces(obj: bpy.types.Object, tags: set, nonempty=False) -> bp # 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(): From 7e14fe61ffae2622d489da0e139f85959e8c43fa Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 133/727] Add 45 lines to infinigen/core/constraints/usage_lookup.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/usage_lookup.py | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 infinigen/core/constraints/usage_lookup.py diff --git a/infinigen/core/constraints/usage_lookup.py b/infinigen/core/constraints/usage_lookup.py new file mode 100644 index 000000000..be91c92a0 --- /dev/null +++ b/infinigen/core/constraints/usage_lookup.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 collections import defaultdict + +_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) + + return _factory_lookup[fac].union({t.FromGenerator(fac)}) + + 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 From ae4c2d74a7b49b1728a481a5b17ad545172d513f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 134/727] Add 3 lines to infinigen/core/constraints/usage_lookup.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/usage_lookup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/usage_lookup.py b/infinigen/core/constraints/usage_lookup.py index be91c92a0..f2c0a83b1 100644 --- a/infinigen/core/constraints/usage_lookup.py +++ b/infinigen/core/constraints/usage_lookup.py @@ -5,6 +5,7 @@ # 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 @@ -21,8 +22,10 @@ def initialize_from_dict(d): _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: From 053cb518f984e0073c283c55195390370ab48844 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 135/727] Add 126 lines to infinigen/core/constraints/checks.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/checks.py | 126 +++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 infinigen/core/constraints/checks.py 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) From 6ed28777e58e0f93408dda045776b8aaf09f4509 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 136/727] Add 59 lines to infinigen/core/constraints/evaluator/domain_contains.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/evaluator/domain_contains.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/domain_contains.py diff --git a/infinigen/core/constraints/evaluator/domain_contains.py b/infinigen/core/constraints/evaluator/domain_contains.py new file mode 100644 index 000000000..024ce4a97 --- /dev/null +++ b/infinigen/core/constraints/evaluator/domain_contains.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: 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 + +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 From a8bbb44cf33129076294770bf3c4ae1eb591dd0c Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 137/727] Add 1 lines to infinigen/core/constraints/evaluator/domain_contains.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/evaluator/domain_contains.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/evaluator/domain_contains.py b/infinigen/core/constraints/evaluator/domain_contains.py index 024ce4a97..f4a8b167a 100644 --- a/infinigen/core/constraints/evaluator/domain_contains.py +++ b/infinigen/core/constraints/evaluator/domain_contains.py @@ -14,6 +14,7 @@ reasoning as r, ) from infinigen.core.constraints.example_solver import state_def +from infinigen.core import tags as t logger = logging.getLogger(__name__) From fc097d36e20665fa9e01bd89a9bf9a2362b47bb3 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 138/727] Add 10 lines to infinigen/core/constraints/evaluator/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/evaluator/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/__init__.py 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 From d94c095c32e53780c28ff0c0bb751d3b29ae47a3 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 139/727] Add 115 lines to infinigen/core/constraints/evaluator/eval_memo.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/evaluator/eval_memo.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/eval_memo.py 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 From eb3268406bd64ae9ebfd1dfce0109ea854941031 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 140/727] Add 231 lines to infinigen/core/constraints/evaluator/indoor_util.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/evaluator/indoor_util.py | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/indoor_util.py 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 From 991d8369d3e6207a56e46947275bd413daf750f3 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 141/727] Add 224 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/evaluator/evaluate.py | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/evaluate.py diff --git a/infinigen/core/constraints/evaluator/evaluate.py b/infinigen/core/constraints/evaluator/evaluate.py new file mode 100644 index 000000000..1621d28a6 --- /dev/null +++ b/infinigen/core/constraints/evaluator/evaluate.py @@ -0,0 +1,224 @@ +# Copyright (c) Princeton University. +# This 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 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__) + + cl.ForAll, + cl.SumOver, + cl.MeanOver, + 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): + 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): + + 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) + raise ValueError( + f'_compute_node_val encountered undefined variable {node}. {memo.keys()}' + ) + name: evaluate_node(c, state, memo) + 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 = {} + val = _compute_node_val(node, state, memo) + 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 From 6c26c8cf868cd419f34c7d7e81977ef8f050f9cc Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 142/727] Add 26 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../core/constraints/evaluator/evaluate.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/infinigen/core/constraints/evaluator/evaluate.py b/infinigen/core/constraints/evaluator/evaluate.py index 1621d28a6..49e4fb754 100644 --- a/infinigen/core/constraints/evaluator/evaluate.py +++ b/infinigen/core/constraints/evaluator/evaluate.py @@ -6,6 +6,7 @@ from typing import Type, Callable from dataclasses import dataclass +import copy import logging import operator import pandas as pd @@ -19,21 +20,28 @@ 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) @@ -46,12 +54,24 @@ def _compute_node_val(node: cl.Node, state: State, memo: dict): 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() + } + 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 @@ -163,12 +183,18 @@ 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 From a7d1bb4eb8636f70d4277f15746d0eb622f84323 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 143/727] Add 5 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/evaluator/evaluate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/core/constraints/evaluator/evaluate.py b/infinigen/core/constraints/evaluator/evaluate.py index 49e4fb754..6c57343ba 100644 --- a/infinigen/core/constraints/evaluator/evaluate.py +++ b/infinigen/core/constraints/evaluator/evaluate.py @@ -10,6 +10,7 @@ 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 @@ -64,6 +65,10 @@ def _compute_node_val(node: cl.Node, state: State, memo: dict): 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 _: From 75ec61eb333eb18965baac4c3db6de5999458ddd Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 144/727] Add 262 lines to infinigen/core/constraints/evaluator/node_impl/symmetry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../evaluator/node_impl/symmetry.py | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/node_impl/symmetry.py 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..43fe6c609 --- /dev/null +++ b/infinigen/core/constraints/evaluator/node_impl/symmetry.py @@ -0,0 +1,262 @@ +# Copyright (c) Princeton University. +# This 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 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 + From 3c551af5c4ed214432d2ef7a40132fbe1a920eae Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 145/727] Add 1 lines to infinigen/core/constraints/evaluator/node_impl/symmetry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/evaluator/node_impl/symmetry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/symmetry.py b/infinigen/core/constraints/evaluator/node_impl/symmetry.py index 43fe6c609..ec12f6017 100644 --- a/infinigen/core/constraints/evaluator/node_impl/symmetry.py +++ b/infinigen/core/constraints/evaluator/node_impl/symmetry.py @@ -7,6 +7,7 @@ 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 From 9ac5d551453ec8283d217711f7c37d4be518896a Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 146/727] Add 993 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../evaluator/node_impl/trimesh_geometry.py | 993 ++++++++++++++++++ 1 file changed, 993 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py 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..716fa0e68 --- /dev/null +++ b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py @@ -0,0 +1,993 @@ +# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. +# 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 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 + + +import infinigen.core.constraints.constraint_language.util as iu +from infinigen.core.constraints.example_solver import state_def +import infinigen.core.constraints.evaluator.node_impl.symmetry as symmetry + +# from scipy.optimize import dual_annealing +# from tqdm import tqdm + +logger = logging.getLogger(__name__) + + + 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 + + + # eliminate symmetrical cases + if ( + a is None or + ): + a, b = b, a + + # nobody wants to be told a 0 distance if they query how far a chair is from the set of all chairs + a.remove(b) + + raise ValueError(f'query recieved empty input {a=}') + 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 + + + +@dataclass +class ContactResult: + hit: bool + names: list[str] + contacts: list + +def any_touching( + scene: Scene, + a: Union[str, list[str]], +): + + ''' + 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 + ''' + 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 + + scene: Scene, + a: Union[str, list[str]], + b: Union[str, list[str]] = 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) + 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] + 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}") + 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, + 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_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 + + state: state_def.State, + b: Union[str, list[str]], + 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 + + """ + 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 + 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( + for name, mesh in zip(a, a_trimeshes): + 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): + return False + if relation_state.child_plane_idx >= len(obj_all_planes): + 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 From 856eeb7a2279c46bf6dd0b037d560a072ce4f492 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 147/727] Add 170 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../evaluator/node_impl/trimesh_geometry.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py index 716fa0e68..adea440a6 100644 --- a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py +++ b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py @@ -1,5 +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: +# - 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 @@ -7,6 +13,7 @@ from copy import copy import numpy as np +import gin import bpy import trimesh @@ -28,13 +35,42 @@ 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 + 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)]] a_front_planes = state.planes.get_tagged_planes(obj, tag) if len(a_front_planes) > 1: @@ -46,17 +82,33 @@ 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 @@ -65,7 +117,12 @@ 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: @@ -76,6 +133,10 @@ class ContactResult: 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 ): ''' @@ -83,6 +144,8 @@ def any_touching( 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: @@ -120,9 +183,13 @@ class DistanceResult: 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 ): ''' @@ -133,6 +200,7 @@ class DistanceResult: # 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) @@ -167,6 +235,7 @@ class DistanceResult: return DistanceResult( dist=dist, + names=list(data.names) if data is not None else None, data=data ) @@ -562,8 +631,13 @@ def angle_alignment_cost_base(state: state_def.State, a: Union[str, list[str]], 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) @@ -784,7 +858,88 @@ def dist_soft_score(res: dict, min, max): 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 @@ -809,7 +964,12 @@ def dist_soft_score(res: dict, min, max): 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 @@ -827,6 +987,7 @@ def dist_soft_score(res: dict, min, max): 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]) @@ -858,7 +1019,12 @@ def center_stable_surface(scene, a, state): 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 @@ -871,9 +1037,12 @@ def center_stable_surface(scene, a, state): 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) @@ -883,6 +1052,7 @@ def center_stable_surface(scene, a, state): score += np.linalg.norm(obj_centroid - parent_centroid) iu.delete_obj(scene, splitted_parent.name) + return score From 71c907b9adcb19b467b43d12586d5d13019688a2 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 148/727] Add 9 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../constraints/evaluator/node_impl/trimesh_geometry.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py index adea440a6..abdf935c0 100644 --- a/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py +++ b/infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py @@ -31,14 +31,18 @@ # 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 @@ -72,6 +76,8 @@ def get_cardinal_planes_bbox(vertices: np.ndarray): 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)})') @@ -214,6 +220,7 @@ def min_dist( 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) @@ -225,6 +232,7 @@ def min_dist( 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: @@ -508,6 +516,7 @@ def angle_alignment_cost_tagged(state: state_def.State, a: Union[str, list[str]] 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 = [] From b64ae48745edd1495a14f6a602db989783dd54e1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 149/727] Add 1 lines to infinigen/core/constraints/evaluator/node_impl/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/evaluator/node_impl/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 infinigen/core/constraints/evaluator/node_impl/__init__.py 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 From ac3d6baa0e67fa0e7987a9f556a6df69e6fc8f8a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 150/727] Add 277 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../evaluator/node_impl/impl_bindings.py | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 infinigen/core/constraints/evaluator/node_impl/impl_bindings.py 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..59a1f89ac --- /dev/null +++ b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py @@ -0,0 +1,277 @@ +# This 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.constraints.evaluator import domain_contains + +from . import trimesh_geometry, symmetry + +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) + cons: cl.center_stable_surface_dist, + child_vals: dict, + use_collision_impl: bool = True + + + 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) + cons: cl.Node, + state: state_def.State, + others_tags: set = None +): + + objs = statenames_to_blnames(state, child_vals['objs']) + others = statenames_to_blnames(state, child_vals['others']) + + logger.debug('min_distance had no targets') + + 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) + 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 trimesh_geometry.min_dist( + ).dist + +@register_node_impl(cl.min_dist_2d) +def min_dist_2d_impl( + state: state_def.State, + child_vals: dict +): + a = statenames_to_blnames(state, child_vals['objs']) + b = statenames_to_blnames(state, child_vals['others']) + return trimesh_geometry.min_dist_2d( + ) + +@register_node_impl(cl.focus_score) +def focus_score_impl( + 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( + a=a, + b=b + ) + +def angle_alignment_impl( + state: state_def.State, +): + 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 + ) + +@register_node_impl(cl.freespace_2d) +def freespace_2d_impl( + state: state_def.State, + child_vals: dict +): + return trimesh_geometry.freespace_2d() + +@register_node_impl(cl.rotational_asymmetry) +def rotational_asymmetry_impl( + 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) + + use_long_plane: bool = True, + +@register_node_impl(cl.tagged) +def tagged_impl( + 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 + + state: state_def.State, + + state: state_def.State, +@register_node_impl(cl.related_to) + cons: cl.related_to, + state: state_def.State, + child_vals: dict +): + children: set[str] = child_vals['child'] + parents: set[str] = child_vals['parent'] + res = set() + for o in children: + rs.relation.implies(r) and rs.target_name in parents + ): + 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, + 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]) + } From c782c9ddac54c01eb6fa55cc73e693c237ddbf56 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 151/727] Add 52 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../evaluator/node_impl/impl_bindings.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py index 59a1f89ac..e5b5b9501 100644 --- a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py +++ b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py @@ -72,11 +72,28 @@ def operator_impl( 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( @@ -92,9 +109,11 @@ def operator_impl( state.trimesh_scene, objs, others, cons.normal ) return res + @register_node_impl(cl.distance) cons: cl.Node, state: state_def.State, + child_vals: dict, others_tags: set = None ): @@ -149,17 +168,24 @@ def focus_score_impl( 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) @@ -179,7 +205,33 @@ def rotational_asymmetry_impl( 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( From d5cd544da127860db4215d2e247ba79de9cd1600 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 152/727] Add 42 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../evaluator/node_impl/impl_bindings.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py index e5b5b9501..c7c9111e9 100644 --- a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py +++ b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py @@ -17,6 +17,7 @@ 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 @@ -111,6 +112,7 @@ def accessibility_impl( return res @register_node_impl(cl.distance) +def min_distance_impl( cons: cl.Node, state: state_def.State, child_vals: dict, @@ -120,7 +122,9 @@ def accessibility_impl( 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, @@ -136,27 +140,35 @@ def accessibility_impl( 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, ): @@ -190,6 +202,7 @@ def angle_alignment_impl( @register_node_impl(cl.freespace_2d) def freespace_2d_impl( + cons: cl.freespace_2d, state: state_def.State, child_vals: dict ): @@ -197,6 +210,7 @@ def freespace_2d_impl( @register_node_impl(cl.rotational_asymmetry) def rotational_asymmetry_impl( + cons: cl.rotational_asymmetry, state: state_def.State, child_vals: dict ): @@ -235,6 +249,7 @@ def coplanarity_cost_impl( @register_node_impl(cl.tagged) def tagged_impl( + cons: cl.tagged, state: state_def.State, child_vals: dict ): @@ -247,29 +262,56 @@ def tagged_impl( 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) From 9c182e97c6ad56ddb92f12e8d674039337cc6976 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:14 -0700 Subject: [PATCH 153/727] Add 3 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../core/constraints/evaluator/node_impl/impl_bindings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py index c7c9111e9..3ee9e0f04 100644 --- a/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py +++ b/infinigen/core/constraints/evaluator/node_impl/impl_bindings.py @@ -1,3 +1,5 @@ +# Copyright (c) Princeton University. + # This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. # Authors: @@ -21,6 +23,7 @@ from infinigen.core.constraints.evaluator import domain_contains from . import trimesh_geometry, symmetry +import inspect logger = logging.getLogger(__name__) From 1e070fc3925654629892f24de9706c3fb12bd882 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 154/727] Add 64 lines to infinigen/core/constraints/reasoning/expr_equal.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/reasoning/expr_equal.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/expr_equal.py 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 From 6f25ec36c47b4d6cde458c98967fe3865c1cb1e7 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 155/727] Add 416 lines to infinigen/core/constraints/reasoning/domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/reasoning/domain.py | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/domain.py 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 From eb8dd3aa24fb77ec0de2c49ab2f27611a17aeba0 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 156/727] Add 74 lines to infinigen/core/constraints/reasoning/constraint_domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../reasoning/constraint_domain.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/constraint_domain.py diff --git a/infinigen/core/constraints/reasoning/constraint_domain.py b/infinigen/core/constraints/reasoning/constraint_domain.py new file mode 100644 index 000000000..6838a0922 --- /dev/null +++ b/infinigen/core/constraints/reasoning/constraint_domain.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: 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 .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 From 2d6de1ea35486e91c0241f6839f7f82b5e657016 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 157/727] Add 1 lines to infinigen/core/constraints/reasoning/constraint_domain.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/reasoning/constraint_domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/reasoning/constraint_domain.py b/infinigen/core/constraints/reasoning/constraint_domain.py index 6838a0922..e627a3c56 100644 --- a/infinigen/core/constraints/reasoning/constraint_domain.py +++ b/infinigen/core/constraints/reasoning/constraint_domain.py @@ -16,6 +16,7 @@ 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__) From c783cfede086a6239f378cb2f421983f85f0a41d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 158/727] Add 174 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../reasoning/constraint_bounding.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/constraint_bounding.py diff --git a/infinigen/core/constraints/reasoning/constraint_bounding.py b/infinigen/core/constraints/reasoning/constraint_bounding.py new file mode 100644 index 000000000..574bbaee8 --- /dev/null +++ b/infinigen/core/constraints/reasoning/constraint_bounding.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: +# - 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 + +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") + + case cl.ScalarOperatorExpression(f, (lhs, rhs)) if f in int_inverse_op.keys() or f in int_inverse_op: +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 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) + 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 From 30f5401d8f49f294e4803f9e714d96beb50f5bb7 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 159/727] Add 35 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by David Yan. --- .../reasoning/constraint_bounding.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/core/constraints/reasoning/constraint_bounding.py b/infinigen/core/constraints/reasoning/constraint_bounding.py index 574bbaee8..049588749 100644 --- a/infinigen/core/constraints/reasoning/constraint_bounding.py +++ b/infinigen/core/constraints/reasoning/constraint_bounding.py @@ -114,7 +114,34 @@ def _expression_map_bound_binop( 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: @@ -133,6 +160,11 @@ def expression_map_bound(node: cl.Node, bound: Bound) -> list[Bound]: # 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, @@ -149,6 +181,9 @@ def constraint_bounds( 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: From f7533c0f36b07096a1873bc7731c1f9bf7a3cf8a Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 160/727] Add 1 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/reasoning/constraint_bounding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/reasoning/constraint_bounding.py b/infinigen/core/constraints/reasoning/constraint_bounding.py index 049588749..a283ae853 100644 --- a/infinigen/core/constraints/reasoning/constraint_bounding.py +++ b/infinigen/core/constraints/reasoning/constraint_bounding.py @@ -22,6 +22,7 @@ from .domain_substitute import domain_tag_substitute from .constraint_constancy import is_constant +from infinigen.core import tags as t logger = logging.getLogger(__name__) From 2b0b14378f9709a5d55b7a719cc8dd855b03f923 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 161/727] Add 22 lines to infinigen/core/constraints/reasoning/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/reasoning/__init__.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/__init__.py 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 From 9064d5982d541de731191f1e6fd82e18165f3f0a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 162/727] Add 21 lines to infinigen/core/constraints/reasoning/constraint_constancy.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../reasoning/constraint_constancy.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/constraint_constancy.py 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 From 0d6167bb9bb8cabbaf7648eaac1220759646bbc6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 163/727] Add 66 lines to infinigen/core/constraints/reasoning/domain_substitute.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../reasoning/domain_substitute.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 infinigen/core/constraints/reasoning/domain_substitute.py 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 From 0217ecc89393aeaf9a4117591f6002e6c3a406ae Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 164/727] Add 365 lines to infinigen/core/constraints/example_solver/annealing.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/annealing.py | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/annealing.py diff --git a/infinigen/core/constraints/example_solver/annealing.py b/infinigen/core/constraints/example_solver/annealing.py new file mode 100644 index 000000000..714dc3ce1 --- /dev/null +++ b/infinigen/core/constraints/example_solver/annealing.py @@ -0,0 +1,365 @@ + +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 + + +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 From be59c8e00fe9b47b9215ae1adb4be9c6650aeb70 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 165/727] Add 7 lines to infinigen/core/constraints/example_solver/annealing.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/annealing.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/constraints/example_solver/annealing.py b/infinigen/core/constraints/example_solver/annealing.py index 714dc3ce1..2db976694 100644 --- a/infinigen/core/constraints/example_solver/annealing.py +++ b/infinigen/core/constraints/example_solver/annealing.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, Karhan Kayan from collections import defaultdict import logging @@ -23,6 +28,7 @@ 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__) @@ -307,6 +313,7 @@ def step(self, consgraph, state, move_gen_func, filter_domain): f"{move_log}" ) + if is_log_step: self.stats.append(dict( curr_iteration=self.curr_iteration, From 76bb2f7d9473ac82fbebf91ca6548cf75f2818e1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 166/727] Add 151 lines to infinigen/core/constraints/example_solver/propose_relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/propose_relations.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/propose_relations.py 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], + ) From b41ae10f93b7e91979602592032782739362c147 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 167/727] Add 3 lines to infinigen/core/constraints/example_solver/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/__init__.py 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 From aea06f83181ca06fb3190a6bca0a767c9ed05a2c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 168/727] Add 172 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/state_def.py | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/state_def.py 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..5b3236b25 --- /dev/null +++ b/infinigen/core/constraints/example_solver/state_def.py @@ -0,0 +1,172 @@ +# This 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 +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 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 + +logger = logging.getLogger(__name__) + + 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 + relations: list[RelationState] = field(default_factory=list) + dof_matrix_translation: np.array = None + dof_rotation_axis: np.array = None + _pose_affects_score = 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 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 + + 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 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) + 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 + + 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) + 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.add(t.SpecificObject(obj.name)) + objs[obj.name] = ObjectState( + obj=obj, + tags=tags + ) From 9ff193c193fd65d5bdc9ed760068ae7690a614f1 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 169/727] Add 17 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../constraints/example_solver/state_def.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/infinigen/core/constraints/example_solver/state_def.py b/infinigen/core/constraints/example_solver/state_def.py index 5b3236b25..8883b4d5c 100644 --- a/infinigen/core/constraints/example_solver/state_def.py +++ b/infinigen/core/constraints/example_solver/state_def.py @@ -27,6 +27,7 @@ reasoning as r ) from .geometry import parse_scene +import trimesh logger = logging.getLogger(__name__) @@ -43,6 +44,9 @@ class ObjectState: dof_rotation_axis: np.array = 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() @@ -65,6 +69,8 @@ class State: objs: OrderedDict[str, ObjectState] trimesh_scene: trimesh.Scene = None + bvh_cache : dict = field(default_factory=dict) + planes: Planes = None def print(self): @@ -132,6 +138,9 @@ def preprocess_field(x): 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(): @@ -140,6 +149,7 @@ def __post_init__(self): 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] @@ -148,6 +158,9 @@ def __post_init__(self): *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 @@ -160,6 +173,7 @@ def __post_init__(self): ) o.obj = bpy.data.objects[o.obj] + def state_from_dummy_scene(col: bpy.types.Collection) -> State: objs = {} @@ -168,5 +182,8 @@ def state_from_dummy_scene(col: bpy.types.Collection) -> State: tags.add(t.SpecificObject(obj.name)) objs[obj.name] = ObjectState( obj=obj, + generator=None, tags=tags ) + return State(objs=objs) + From a50e42d71e4eba73774c2bafc702a979d56215ef Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 170/727] Add 9 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/state_def.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/core/constraints/example_solver/state_def.py b/infinigen/core/constraints/example_solver/state_def.py index 8883b4d5c..90da3b478 100644 --- a/infinigen/core/constraints/example_solver/state_def.py +++ b/infinigen/core/constraints/example_solver/state_def.py @@ -5,6 +5,7 @@ # - 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 @@ -28,17 +29,23 @@ ) 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 @@ -53,6 +60,7 @@ class ObjectState: 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): @@ -179,6 +187,7 @@ 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, From 2869c361b46f439ebcf24e05191e01cd43fa2177 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 171/727] Add 7 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/core/constraints/example_solver/state_def.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/constraints/example_solver/state_def.py b/infinigen/core/constraints/example_solver/state_def.py index 90da3b478..5639df897 100644 --- a/infinigen/core/constraints/example_solver/state_def.py +++ b/infinigen/core/constraints/example_solver/state_def.py @@ -1,9 +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: # - Alexander Raistrick: state, print, to_json # - Karhan Kayan: add dof / trimesh + from __future__ import annotations from dataclasses import dataclass, field import typing @@ -19,6 +21,7 @@ import numpy as np import bpy +import shapely import trimesh from infinigen.core.constraints.example_solver.geometry.planes import Planes @@ -47,8 +50,10 @@ class ObjectState: 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 @@ -159,6 +164,7 @@ def save(self, filename: str): with open(filename, 'wb') as file: pickle.dump(self, file) + for os in self.objs.values(): os.obj = bpy.data.objects[os.obj] @@ -166,6 +172,7 @@ def save(self, filename: str): *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: From 54b9acf1cd0306b8b265e8d053a52d2074aee096 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 172/727] Add 2 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/core/constraints/example_solver/state_def.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/core/constraints/example_solver/state_def.py b/infinigen/core/constraints/example_solver/state_def.py index 5639df897..bc325d6d8 100644 --- a/infinigen/core/constraints/example_solver/state_def.py +++ b/infinigen/core/constraints/example_solver/state_def.py @@ -111,6 +111,8 @@ 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(): From ad0bf5311f9fb6b5adda58c86417db5a8e6b5380 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 173/727] Add 333 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/propose_discrete.py | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/propose_discrete.py 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..333a9611d --- /dev/null +++ b/infinigen/core/constraints/example_solver/propose_discrete.py @@ -0,0 +1,333 @@ +# Copyright (c) Princeton University. +# This 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 + 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 + +logger = logging.getLogger(__name__) + +class DummyCubeGenerator(AssetFactory): + + def __init__(self, seed): + super().__init__(seed) + + return butil.spawn_cube() + + 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) + 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) + 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) + + +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 + 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] + 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 From 5893bdbf9573d8be54fa560a6cb03fdeca77973b Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 174/727] Add 12 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../constraints/example_solver/propose_discrete.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/core/constraints/example_solver/propose_discrete.py b/infinigen/core/constraints/example_solver/propose_discrete.py index 333a9611d..a48990c97 100644 --- a/infinigen/core/constraints/example_solver/propose_discrete.py +++ b/infinigen/core/constraints/example_solver/propose_discrete.py @@ -15,9 +15,12 @@ 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 ) @@ -26,8 +29,10 @@ 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__) @@ -36,8 +41,12 @@ 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) @@ -91,6 +100,8 @@ def propose_addition_bound_gen( 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) @@ -289,6 +300,7 @@ def propose_resample( 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) From bc024ec802b778fa2127be8a9afec6c66813e12a Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 175/727] Add 5 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/example_solver/propose_discrete.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/core/constraints/example_solver/propose_discrete.py b/infinigen/core/constraints/example_solver/propose_discrete.py index a48990c97..8b3b6d8f2 100644 --- a/infinigen/core/constraints/example_solver/propose_discrete.py +++ b/infinigen/core/constraints/example_solver/propose_discrete.py @@ -73,6 +73,8 @@ def lookup_generator(preds: set[t.Semantics]): 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 @@ -210,6 +212,7 @@ def propose_addition( 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: @@ -225,6 +228,7 @@ def propose_addition( 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, @@ -268,6 +272,7 @@ def propose_relation_plane_change( 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 From 806ab240ba6aabe479691ada1940aa9cba33f226 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 176/727] Add 148 lines to infinigen/core/constraints/example_solver/propose_continous.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/propose_continous.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/propose_continous.py 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 From 22ac2bb5b5bccb9557f3d264951e0deac6fa5821 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 177/727] Add 216 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/example_solver/solve.py | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/solve.py diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py new file mode 100644 index 000000000..099488d31 --- /dev/null +++ b/infinigen/core/constraints/example_solver/solve.py @@ -0,0 +1,216 @@ +# Copyright (c) Princeton University. +# This 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 copy + +import numpy as np +from tqdm import trange, tqdm +import gin +from infinigen.core.constraints import ( + constraint_language as cl, + reasoning as r, + usage_lookup, +) +from infinigen.core.constraints.example_solver.geometry import parse_scene, planes + + +from infinigen.core.constraints.example_solver.state_def import State + 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 .annealing import SimulatedAnnealingSolver + +logger = logging.getLogger(__name__) +def map_range(x, xmin, xmax, ymin, ymax, exp=1): + + 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.state: State = 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 + + 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) + + + 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 + ] + From 726ab0b8c8f1b3aa7672a9b07dfb725877f85a35 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 178/727] Add 8 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/core/constraints/example_solver/solve.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py index 099488d31..8b6d4a7de 100644 --- a/infinigen/core/constraints/example_solver/solve.py +++ b/infinigen/core/constraints/example_solver/solve.py @@ -7,16 +7,22 @@ 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 propose_continous, @@ -28,6 +34,7 @@ from .annealing import SimulatedAnnealingSolver logger = logging.getLogger(__name__) + def map_range(x, xmin, xmax, ymin, ymax, exp=1): return ymin @@ -77,6 +84,7 @@ def __init__( self.optim = SimulatedAnnealingSolver( output_folder=output_folder, ) + self.room_solver_fn = MultistoryRoomSolver if multistory else RoomSolver self.state: State = None self.moves = self._configure_move_weights(restrict_moves) From 210b96bfadca07148089053983788344251581e7 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 179/727] Add 8 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/solve.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py index 8b6d4a7de..0c09ce35b 100644 --- a/infinigen/core/constraints/example_solver/solve.py +++ b/infinigen/core/constraints/example_solver/solve.py @@ -4,6 +4,7 @@ # Authors: Alexander Raistrick +import logging from pathlib import Path import copy @@ -25,12 +26,17 @@ from .room import RoomSolver, MultistoryRoomSolver from infinigen.core.constraints.example_solver.state_def import State +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__) @@ -86,6 +92,7 @@ def __init__( ) self.room_solver_fn = MultistoryRoomSolver if multistory else RoomSolver self.state: State = None + self.moves = self._configure_move_weights(restrict_moves) @@ -195,6 +202,7 @@ def solve_objects( 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 = { From 6e957b5a73340d8b77e82fb1caa93dcc79ce6327 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 180/727] Add 6 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/core/constraints/example_solver/solve.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py index 0c09ce35b..da95402e5 100644 --- a/infinigen/core/constraints/example_solver/solve.py +++ b/infinigen/core/constraints/example_solver/solve.py @@ -43,9 +43,11 @@ 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 @@ -62,6 +64,7 @@ def __call__(self, t): @gin.configurable class Solver: + def __init__( self, output_folder: Path, @@ -92,6 +95,8 @@ def __init__( ) 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) @@ -223,6 +228,7 @@ def solve_objects( 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) From ef8dff5e9e5b3a1afb607d8167f5e4e4794b3eec Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:15 -0700 Subject: [PATCH 181/727] Add 4 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/solve.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/solve.py b/infinigen/core/constraints/example_solver/solve.py index da95402e5..4a4c6e8ef 100644 --- a/infinigen/core/constraints/example_solver/solve.py +++ b/infinigen/core/constraints/example_solver/solve.py @@ -26,6 +26,8 @@ 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, @@ -140,6 +142,7 @@ def _configure_move_weights(self, restrict_moves): return schedules + @gin.configurable def choose_move_type( self, it: int, @@ -154,6 +157,7 @@ def choose_move_type( 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, From d4670ae74d7d0ff37fd75c34579b443e2d154726 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 182/727] Add 115 lines to infinigen/core/constraints/example_solver/populate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/populate.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/populate.py diff --git a/infinigen/core/constraints/example_solver/populate.py b/infinigen/core/constraints/example_solver/populate.py new file mode 100644 index 000000000..53479ccde --- /dev/null +++ b/infinigen/core/constraints/example_solver/populate.py @@ -0,0 +1,115 @@ +# - Alexander Raistrick: populate_state_placeholders, apply_cutter +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 + + # 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) From 94a8f3108841534976ba5adbecf272ecf7566d1c Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 183/727] Add 6 lines to infinigen/core/constraints/example_solver/populate.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/core/constraints/example_solver/populate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/populate.py b/infinigen/core/constraints/example_solver/populate.py index 53479ccde..cb0fcdd07 100644 --- a/infinigen/core/constraints/example_solver/populate.py +++ b/infinigen/core/constraints/example_solver/populate.py @@ -1,4 +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: # - Alexander Raistrick: populate_state_placeholders, apply_cutter +# - Stamatis Alexandropoulos: Initial version of window cutting + import logging import bpy From 0e1e80d504e799b473395920dd343c8980805293 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 184/727] Add 3 lines to infinigen/core/constraints/example_solver/populate.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/populate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/example_solver/populate.py b/infinigen/core/constraints/example_solver/populate.py index cb0fcdd07..affbc6d9d 100644 --- a/infinigen/core/constraints/example_solver/populate.py +++ b/infinigen/core/constraints/example_solver/populate.py @@ -106,6 +106,9 @@ def populate_state_placeholders(state: State, filter=None, final=True): 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'): From c0d15c0b95d6699848d2512bad92de0faa6fb21f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 185/727] Add 37 lines to infinigen/core/constraints/example_solver/moves/moves.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/moves/moves.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/moves.py 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 From 1c449622edb08cdf05eeec0a0f9cc7aefe3effdf Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 186/727] Add 106 lines to infinigen/core/constraints/example_solver/moves/reassignment.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/moves/reassignment.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/reassignment.py 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..b4b725175 --- /dev/null +++ b/infinigen/core/constraints/example_solver/moves/reassignment.py @@ -0,0 +1,106 @@ +# Copyright (c) Princeton University. +# This 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 + translate, + rotate, +) + +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'] + + +@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) + + 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 From 3cc35816eebbbc383c3be24544cd26782c051915 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 187/727] Add 4 lines to infinigen/core/constraints/example_solver/moves/reassignment.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/example_solver/moves/reassignment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/moves/reassignment.py b/infinigen/core/constraints/example_solver/moves/reassignment.py index b4b725175..421391db9 100644 --- a/infinigen/core/constraints/example_solver/moves/reassignment.py +++ b/infinigen/core/constraints/example_solver/moves/reassignment.py @@ -13,8 +13,10 @@ 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 @@ -45,6 +47,7 @@ def restore_pose_backup(state, name, bak): 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): @@ -68,6 +71,7 @@ def apply(self, state: State): rels.parent_plane_idx = self.plane_idx success = dof.try_apply_relation_constraints(state, target_name) + return success def revert(self, state: State): From 7b487dfccdab80461bbce689b04a00c72c81538f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 188/727] Add 62 lines to infinigen/core/constraints/example_solver/moves/swap.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/moves/swap.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/swap.py 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 From 569c25d01cdeaa91257336b8ab0df2fdbbf96efd Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 189/727] Add 6 lines to infinigen/core/constraints/example_solver/moves/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/moves/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/__init__.py 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 From cc7bf3da25da09217044d0c6f147ad28a73dfb90 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 190/727] Add 168 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/moves/addition.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/addition.py 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..a89c943f4 --- /dev/null +++ b/infinigen/core/constraints/example_solver/moves/addition.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: Alexander Raistrick + +from dataclasses import dataclass, field +import numpy as np +import typing +import logging + +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.constraints import ( + constraint_language as cl, + usage_lookup +) + +from infinigen.core.util import blender as butil +) +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 + +logger = logging.getLogger(__name__) + +GLOBAL_GENERATOR_SINGLETON_CACHE = {} + +def sample_rand_placeholder(gen_class: type[AssetFactory]): + + + 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) + + 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) + + + 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) + + def revert(self, state: State): + 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=}') + + 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))) From 1dff4397a8b8c89071344df59aa76d616c22b6f1 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 191/727] Add 15 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../constraints/example_solver/moves/addition.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/core/constraints/example_solver/moves/addition.py b/infinigen/core/constraints/example_solver/moves/addition.py index a89c943f4..4a3658d7a 100644 --- a/infinigen/core/constraints/example_solver/moves/addition.py +++ b/infinigen/core/constraints/example_solver/moves/addition.py @@ -8,6 +8,7 @@ import numpy as np import typing import logging +import gin from pprint import pprint @@ -25,6 +26,9 @@ ) 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 @@ -37,6 +41,9 @@ 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__) @@ -55,6 +62,8 @@ def sample_rand_placeholder(gen_class: type[AssetFactory]): inst_seed = np.random.randint(1e7) + new_obj = gen.spawn_placeholder(inst_seed, loc=(0,0,0), rot=(0,0,0)) + new_obj = gen.spawn_asset(inst_seed, loc=(0,0,0), rot=(0,0,0)) 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) @@ -96,6 +105,7 @@ def apply(self, state: State): 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__)) @@ -109,8 +119,11 @@ def apply(self, state: State): 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 @@ -149,7 +162,9 @@ def apply(self, state: State): 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): From 6948affc066cabb9ca65366b92ba8c7da5b63e50 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 192/727] Add 5 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/moves/addition.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/core/constraints/example_solver/moves/addition.py b/infinigen/core/constraints/example_solver/moves/addition.py index 4a3658d7a..26326a017 100644 --- a/infinigen/core/constraints/example_solver/moves/addition.py +++ b/infinigen/core/constraints/example_solver/moves/addition.py @@ -19,6 +19,7 @@ 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, @@ -51,6 +52,7 @@ 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] @@ -62,8 +64,11 @@ def sample_rand_placeholder(gen_class: type[AssetFactory]): 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) From f6b1feafd4dd998e5c2f3b7389b2362af23cbc58 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 193/727] Add 116 lines to infinigen/core/constraints/example_solver/moves/pose.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/moves/pose.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/pose.py 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..7c142c8e5 --- /dev/null +++ b/infinigen/core/constraints/example_solver/moves/pose.py @@ -0,0 +1,116 @@ + +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 . 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 From 8cf2cbc7327f98b9d46cb7cf07900c197ffeb9d6 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 194/727] Add 6 lines to infinigen/core/constraints/example_solver/moves/pose.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/moves/pose.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/moves/pose.py b/infinigen/core/constraints/example_solver/moves/pose.py index 7c142c8e5..454353538 100644 --- a/infinigen/core/constraints/example_solver/moves/pose.py +++ b/infinigen/core/constraints/example_solver/moves/pose.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, Karhan Kayan from dataclasses import dataclass import numpy as np @@ -10,6 +15,7 @@ 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 a9df86a15407b1022825c72a6aeeee2d13bcdd63 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 195/727] Add 52 lines to infinigen/core/constraints/example_solver/moves/deletion.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/moves/deletion.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/moves/deletion.py 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 From e97691e6ef85e43b7caf01e194ef6af395e76268 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 196/727] Add 321 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/solidifier.py | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/solidifier.py 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..2c4dc36f7 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/solidifier.py @@ -0,0 +1,321 @@ +# Copyright (c) Princeton University. + +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.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.constraints.example_solver.state_def import ObjectState, RelationState, State +from infinigen.core.constraints import constraint_language as cl +from infinigen.core.util.logging import BadSeedError + + +@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.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()} + 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): + for k_ in k: + butil.modify_mesh( + rooms[k_], 'BOOLEAN', object=c, operation='DIFFERENCE', use_self=True, + use_hole_tolerant=True + ) + 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') + + + def convert_solver_state(self, rooms, segments, shared_edges, open_cutters, door_cutters, exterior_cutters): + for k, o in rooms.items(): + 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 + 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): + 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) + + 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) + obj.vertex_groups.remove(obj.vertex_groups['visible_']) + 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(): + 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 + 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 + self.tag(cutter) + 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) + 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) + 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) + 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, 'segment_id', np.arange(len(center)), 'INT', 'FACE') + np.ones_like(ceiling) if visible else np.zeros_like(ceiling), 'INT', 'FACE') + np.zeros_like(ceiling) if visible else np.ones_like(ceiling), 'INT', 'FACE') From 1c4063f8d3936ddd664af11aa2a5f813d7082667 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 197/727] Add 99 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/room/solidifier.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/solidifier.py b/infinigen/core/constraints/example_solver/room/solidifier.py index 2c4dc36f7..29d42c01c 100644 --- a/infinigen/core/constraints/example_solver/room/solidifier.py +++ b/infinigen/core/constraints/example_solver/room/solidifier.py @@ -1,5 +1,9 @@ # Copyright (c) Princeton University. +# Authors: +# - Lingjie Mei: primary author +# - Karhan Kayan: fix constants +import logging from collections import defaultdict, deque from collections.abc import Iterable, Mapping, Sequence @@ -11,6 +15,8 @@ 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, @@ -27,10 +33,16 @@ 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.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: @@ -41,6 +53,7 @@ def __init__(self, graph: RoomGraph, level, has_ceiling=True, combined_room_type 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): @@ -68,23 +81,51 @@ def solidify(self, assignment, info): 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'): @@ -110,9 +151,22 @@ def solidify(self, assignment, info): 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]: @@ -122,9 +176,34 @@ def convert_solver_state(self, rooms, segments, shared_edges, open_cutters, door 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) @@ -141,11 +220,16 @@ def make_room(self, obj, exterior_edges=None): -1) < WALL_THICKNESS * 4).any(-1).astype(int) else: exterior = np.zeros(len(obj.data.polygons), dtype=int) + + 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) 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(): @@ -184,6 +268,11 @@ def make_exterior_cutters(self, exterior_edges, names): 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[:] @@ -219,8 +308,11 @@ def make_staircase_cutters(self, staircase, names): 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) @@ -230,6 +322,7 @@ def make_door_cutter(self, es, direction): 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) return cutter @@ -245,6 +338,7 @@ def make_entrance_cutter(self, ls): 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) return cutter def make_window_cutter(self, ls, is_panoramic): @@ -267,6 +361,7 @@ def make_window_cutter(self, ls, is_panoramic): 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) return cutter def make_open_cutter(self, es): @@ -308,6 +403,7 @@ def make_open_cutter(self, es): 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) return cutter @staticmethod @@ -318,4 +414,7 @@ def tag(obj, visible=True): wall = ~(ceiling | floor) write_attr_data(obj, 'segment_id', np.arange(len(center)), 'INT', 'FACE') 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) From 344111cfcf68b3fea2a7c4356ac320ea71ef9229 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 198/727] Add 12 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../constraints/example_solver/room/solidifier.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/solidifier.py b/infinigen/core/constraints/example_solver/room/solidifier.py index 29d42c01c..10efdb062 100644 --- a/infinigen/core/constraints/example_solver/room/solidifier.py +++ b/infinigen/core/constraints/example_solver/room/solidifier.py @@ -33,6 +33,7 @@ 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 @@ -220,12 +221,15 @@ def make_room(self, obj, exterior_edges=None): -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 @@ -324,6 +328,7 @@ def make_door_cutter(self, es, direction): 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): @@ -339,6 +344,7 @@ def make_entrance_cutter(self, ls): 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): @@ -362,6 +368,7 @@ def make_window_cutter(self, ls, is_panoramic): 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): @@ -404,6 +411,7 @@ def make_open_cutter(self, es): 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 @@ -412,7 +420,11 @@ def tag(obj, visible=True): 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') From 4650967afcc005f99768500b6a1b8c7ca1cb502e Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 199/727] Add 4 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/solidifier.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/solidifier.py b/infinigen/core/constraints/example_solver/room/solidifier.py index 10efdb062..d6eb63637 100644 --- a/infinigen/core/constraints/example_solver/room/solidifier.py +++ b/infinigen/core/constraints/example_solver/room/solidifier.py @@ -1,4 +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: primary author # - Karhan Kayan: fix constants @@ -23,6 +26,7 @@ ) 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 ( From 4793706b471c7ecbe83757d1b5017a4c10bee506 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 200/727] Add 116 lines to infinigen/core/constraints/example_solver/room/configs.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/configs.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/configs.py 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..ba735b5bc --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/configs.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 +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.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.Closet: 4, + 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]] +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.Closet: ('bool', .1), + RoomType.Balcony: ('bool', .2), + RoomType.Utility: ('bool', .2), + }, + }, + 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 +}) + + 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] From 1bd56e88d44ba559ed1b9e991b5af91bf4fae803 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 201/727] Add 16 lines to infinigen/core/constraints/example_solver/room/configs.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/room/configs.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/configs.py b/infinigen/core/constraints/example_solver/room/configs.py index ba735b5bc..8c481faac 100644 --- a/infinigen/core/constraints/example_solver/room/configs.py +++ b/infinigen/core/constraints/example_solver/room/configs.py @@ -7,6 +7,7 @@ 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 @@ -19,7 +20,9 @@ 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, @@ -28,6 +31,12 @@ } 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, { @@ -61,10 +70,16 @@ def 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)}, @@ -107,6 +122,7 @@ def make_room_colors(): 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, From b84b999940226fae5e0bc51cc1a205e83cdc0d65 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 202/727] Add 273 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../constraints/example_solver/room/solver.py | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/solver.py 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..0ec9d5571 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/solver.py @@ -0,0 +1,273 @@ +# Copyright (c) Princeton University. + +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.constants import SEGMENT_MARGIN + 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 + 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): + 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') From ea7d670dee4d060f7fbcc9ee29617ee8e679edc0 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 203/727] Add 6 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/solver.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/solver.py b/infinigen/core/constraints/example_solver/room/solver.py index 0ec9d5571..113b6e9b5 100644 --- a/infinigen/core/constraints/example_solver/room/solver.py +++ b/infinigen/core/constraints/example_solver/room/solver.py @@ -1,4 +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. + from copy import deepcopy from dataclasses import dataclass @@ -11,6 +14,7 @@ from shapely.ops import shared_paths from infinigen.core.constraints.example_solver.room.constants import SEGMENT_MARGIN +import infinigen.core.constraints.example_solver.room.constants as constants linear_extend_y, canonicalize, update_exterior_edges, update_shared_edges, update_staircase_occupancies @@ -152,6 +156,7 @@ def extrude_room(self, i, info, out=True): 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) @@ -262,6 +267,7 @@ def move_staircase(self, infos): 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)) From 9fc29398864c3999e66b86e7b3db875fc9debef6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 204/727] Add 6 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/room/solver.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/solver.py b/infinigen/core/constraints/example_solver/room/solver.py index 113b6e9b5..c431f933a 100644 --- a/infinigen/core/constraints/example_solver/room/solver.py +++ b/infinigen/core/constraints/example_solver/room/solver.py @@ -2,6 +2,9 @@ # This 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 @@ -13,8 +16,11 @@ 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 From 111f1a0b495793e62fe9da87ff8676e70740722b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 205/727] Add 174 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/blueprint.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/blueprint.py 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..a1649d632 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/blueprint.py @@ -0,0 +1,174 @@ +from copy import deepcopy + +import bpy +import numpy as np +from numpy.random import uniform +from shapely import Polygon +from tqdm import tqdm, trange + +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 .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 + + + + def __init__(self, factory_seed, n_divide_trials=2500, iters_mult=150, ): + 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.score_scale = 5 + + 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_ + + def solve(self): + 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: + + 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: + 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 From 302498cfba88099ee371be8383f17e98396536f3 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 206/727] Add 29 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/room/blueprint.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/blueprint.py b/infinigen/core/constraints/example_solver/room/blueprint.py index a1649d632..881db3b22 100644 --- a/infinigen/core/constraints/example_solver/room/blueprint.py +++ b/infinigen/core/constraints/example_solver/room/blueprint.py @@ -5,11 +5,14 @@ from numpy.random import uniform from shapely import Polygon from tqdm import tqdm, trange +import gin 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 .constants import WALL_HEIGHT + from .graph import GraphMaker from .scorer import BlueprintScorer, JointBlueprintScorer from .contour import ContourFactory @@ -19,9 +22,14 @@ 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 + +@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)) @@ -36,8 +44,11 @@ def __init__(self, factory_seed, n_divide_trials=2500, iters_mult=150, ): 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: @@ -50,8 +61,26 @@ def __init__(self, factory_seed, n_divide_trials=2500, iters_mult=150, ): 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) + return state, unique_roomtypes, dimensions From a23c4f7ca8aa4a7e82922d7110bd045392622cb8 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:16 -0700 Subject: [PATCH 207/727] Add 9 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/example_solver/room/blueprint.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/blueprint.py b/infinigen/core/constraints/example_solver/room/blueprint.py index 881db3b22..97e83ad41 100644 --- a/infinigen/core/constraints/example_solver/room/blueprint.py +++ b/infinigen/core/constraints/example_solver/room/blueprint.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: Lingjie Mei, Karhan Kayan: fix constants + from copy import deepcopy import bpy @@ -81,6 +87,7 @@ def solve(self): state, rooms_meshed = self.solidifier.solidify(assignment, info) + dimensions = self.width, self.height, constants.WALL_HEIGHT return state, unique_roomtypes, dimensions @@ -153,6 +160,8 @@ def build_contours(self): self.contours.append(contour) break else: + self.widths[i] -= constants.UNIT + self.heights[i] -= constants.UNIT def solve(self): assignments, infos = [], [] From 68751958e612cc9da553a120959120d92c8c674e Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 208/727] Add 6 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/room/blueprint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/blueprint.py b/infinigen/core/constraints/example_solver/room/blueprint.py index 97e83ad41..e2e3dddc8 100644 --- a/infinigen/core/constraints/example_solver/room/blueprint.py +++ b/infinigen/core/constraints/example_solver/room/blueprint.py @@ -13,10 +13,12 @@ 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 @@ -29,6 +31,7 @@ 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 @@ -87,7 +90,9 @@ def solve(self): 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 @@ -188,6 +193,7 @@ def solve(self): 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 From 0581b0e437985d2073441b9de272464fece972a5 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 209/727] Add 165 lines to infinigen/core/constraints/example_solver/room/graph.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../constraints/example_solver/room/graph.py | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/graph.py 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..6e2bf825d --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/graph.py @@ -0,0 +1,165 @@ +# Copyright (c) Princeton University. +# This 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.util.math import FixedSeed +from infinigen.core.util.random import log_uniform, random_general as rg + +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) From a53aedae07866d8823bf71861af438410e4a0a72 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 210/727] Add 2 lines to infinigen/core/constraints/example_solver/room/graph.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/room/graph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/graph.py b/infinigen/core/constraints/example_solver/room/graph.py index 6e2bf825d..7f21ac06f 100644 --- a/infinigen/core/constraints/example_solver/room/graph.py +++ b/infinigen/core/constraints/example_solver/room/graph.py @@ -10,9 +10,11 @@ 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, From 46025a3da0cc516996ea62c976002788231a5cf7 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 211/727] Add 74 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/constants.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/constants.py 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..158a31ffb --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/constants.py @@ -0,0 +1,74 @@ +# Copyright (c) Princeton University. + +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_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 + 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 + 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()) + + + ys = make_np(global_params().values()) + xs = UNIT, SEGMENT_MARGIN, WALL_THICKNESS, WALL_HEIGHT + for x, y in zip(xs, ys): + x.fill(y) + ys = make_np(door_params().values()) + xs = DOOR_WIDTH, DOOR_MARGIN, DOOR_SIZE + for x, y in zip(xs, ys): + x.fill(y) + 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) From 986ad4b27bcec0cb7c24a09f93ff13c7f7bef348 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 212/727] Add 15 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/room/constants.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/constants.py b/infinigen/core/constraints/example_solver/room/constants.py index 158a31ffb..05e7d1e2c 100644 --- a/infinigen/core/constraints/example_solver/room/constants.py +++ b/infinigen/core/constraints/example_solver/room/constants.py @@ -1,4 +1,7 @@ # Copyright (c) Princeton University. +# Authors: +# - Lingjie Mei: primary author +# - Karhan Kayan: bug fixes import gin import numpy as np @@ -26,6 +29,8 @@ def global_params(unit=.5, segment_margin=1.2, wall_thickness=('uniform', .2, .3 @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) @@ -36,6 +41,11 @@ def global_params(unit=.5, segment_margin=1.2, wall_thickness=('uniform', .2, .3 @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) @@ -72,3 +82,8 @@ def staircase_params(staircase_snap=('uniform', .8, 1.2)): 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 From 50889f29fa9cd5f507e82a663d903736b8bf96b0 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 213/727] Add 11 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/example_solver/room/constants.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/constants.py b/infinigen/core/constraints/example_solver/room/constants.py index 05e7d1e2c..c8fec8bc3 100644 --- a/infinigen/core/constraints/example_solver/room/constants.py +++ b/infinigen/core/constraints/example_solver/room/constants.py @@ -1,4 +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: primary author # - Karhan Kayan: bug fixes @@ -15,6 +18,7 @@ def make_np(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 { @@ -70,14 +74,21 @@ def staircase_params(staircase_snap=('uniform', .8, 1.2)): 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): From b69ffac3f04c53640e52f8b3907ab59b9f4ef381 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 214/727] Add 6 lines to infinigen/core/constraints/example_solver/room/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/core/constraints/example_solver/room/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/__init__.py 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 From c2858c25a6782fa16837d0e867b753bafc20ca69 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 215/727] Add 206 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/decorate.py | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/decorate.py 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..b44e872da --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -0,0 +1,206 @@ +# Copyright (c) Princeton University. + +import bmesh +import bpy +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.util.blender import deep_clone_obj +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.constraints import constraint_language as cl + + + 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) + + for wall_fn in set(wall_fns): + 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) + + + + + for floor_fn in set(floor_fns): + 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] + else: + + factories = [random_door_factory()(np.random.randint(1e7)) for _ in range(3)] + 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]: + door = factory(int(j)) + door.rotation_euler[-1] = -rot_z + doors.append(door) + casing = casing_factory(int(j)) + casing.location = 0, 0, -constants.DOOR_SIZE / 2 + casings.append(casing) + factory.finalize_assets(doors) + casing_factory.finalize_assets(casings) + butil.put_in_collection(casings, casing_col) + + + factory = factories[i] + windows = [] + for j in np.nonzero(indices == i)[0]: + dims = cutter_dims[0], cutter_dims[2], cutter_dims[1] * uniform(.1, .2) + window = factory(int(j), dimensions=dims) + window.location[1] = -WALL_THICKNESS / 2 + factory.finalize_assets(windows) + + +def room_stairs(state, rooms_meshed): + 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 + + + col = butil.get_collection('pillars') + for s in pillar_rooms: + factory = PillarFactory(np.random.randint(1e7)) + 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]) From 82d659ea8dc0b323d202b024481e31180dadf823 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 216/727] Add 114 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/room/decorate.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py index b44e872da..30cd791e6 100644 --- a/infinigen/core/constraints/example_solver/room/decorate.py +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -1,4 +1,9 @@ # Copyright (c) Princeton University. +# Authors: +# - Lingjie Mei: primary author +# - Karhan Kayan: fix constants + +import logging import bmesh import bpy @@ -19,16 +24,31 @@ 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 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.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]): + + 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 = [] @@ -40,7 +60,19 @@ for m in m2delete: ms.remove(m) + 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() + } +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]: @@ -52,8 +84,12 @@ wall_fn.apply(rooms_, **kwargs) +def room_ceilings(ceilings: list[bpy.types.Object]): + logger.debug(f'{room_ceilings.__name__} adding materials to {len(ceilings)=}') +def room_floors(floors: list[bpy.types.Object]): + logger.debug(f'{room_floors.__name__} adding materials to {len(floors)=}, using {len(floor_fns)=}') for floor_fn in set(floor_fns): if floor_fn in [tile, plaster]: indices = np.random.randint(0, 3, len(rooms_)) @@ -61,33 +97,79 @@ rooms__ = [r for r, j in zip(rooms_, indices) if j == i] else: + +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 + else: + rot_z *= np.pi / 2 + door = factory(int(j)) + door.parent = placeholders[j] 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: @@ -183,9 +265,18 @@ def room_stairs(state, rooms_meshed): 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])) 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) @@ -204,3 +295,26 @@ def room_stairs(state, rooms_meshed): 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) From b623bce97684fa43fbbd1913bbdbcedb55c5de71 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 217/727] Add 24 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../example_solver/room/decorate.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py index 30cd791e6..760de4682 100644 --- a/infinigen/core/constraints/example_solver/room/decorate.py +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -34,6 +34,7 @@ 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 @@ -42,6 +43,12 @@ 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) @@ -60,11 +67,21 @@ def split_rooms(rooms_meshed: list[bpy.types.Object]): 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) @@ -86,16 +103,22 @@ def room_walls(wall_objs: list[bpy.types.Object]): 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_) def populate_doors( @@ -277,6 +300,7 @@ def room_pillars(state: state_def.State, walls: list[bpy.types.Object]): 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) From beb2f925d557d118d0a994e6cd891932b55e7ce1 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 218/727] Add 11 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- .../core/constraints/example_solver/room/decorate.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py index 760de4682..661a2b94b 100644 --- a/infinigen/core/constraints/example_solver/room/decorate.py +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -7,6 +7,7 @@ import bmesh import bpy +import gin import numpy as np import shapely import trimesh.convex @@ -121,6 +122,7 @@ def room_floors(floors: list[bpy.types.Object]): floor_fn.apply(rooms_) +@gin.configurable def populate_doors( placeholders: list[bpy.types.Object], n_doors=3, @@ -145,7 +147,16 @@ def populate_doors( 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)) From fc281492cc38100ddc74f9fc271c01ba168c2fb6 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 219/727] Add 5 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/decorate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/decorate.py b/infinigen/core/constraints/example_solver/room/decorate.py index 661a2b94b..2d26e0db9 100644 --- a/infinigen/core/constraints/example_solver/room/decorate.py +++ b/infinigen/core/constraints/example_solver/room/decorate.py @@ -1,4 +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: primary author # - Karhan Kayan: fix constants @@ -32,6 +35,7 @@ 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 @@ -161,6 +165,7 @@ def populate_doors( 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) From 85f50303ab8262e2ebc7eec1259a3cf76a41d7d3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 220/727] Add 80 lines to infinigen/core/constraints/example_solver/room/types.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../constraints/example_solver/room/types.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/types.py 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() From 494395464958f5c58145014d076966af3e50f6c7 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 221/727] Add 139 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../constraints/example_solver/room/utils.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/utils.py 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..39d9bdbe9 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/utils.py @@ -0,0 +1,139 @@ +# Copyright (c) Princeton University. + +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 + +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') + + + 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)) From ff13c1b57566f697b644ee8b9bb2302afcafd09c Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 222/727] Add 7 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/utils.py b/infinigen/core/constraints/example_solver/room/utils.py index 39d9bdbe9..08149d479 100644 --- a/infinigen/core/constraints/example_solver/room/utils.py +++ b/infinigen/core/constraints/example_solver/room/utils.py @@ -1,4 +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. + from collections import defaultdict @@ -8,6 +11,7 @@ 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 @@ -55,6 +59,9 @@ def canonicalize(p): raise NotImplementedError('Invalid multi polygon') +def unit_cast(x, unit=None): + if unit is None: + unit = constants.UNIT return int(x / unit) * unit From 54b1e29e9d6c95fc112be48b73bd7e09e3f26eed Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 223/727] Add 3 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/room/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/utils.py b/infinigen/core/constraints/example_solver/room/utils.py index 08149d479..9353c9fb5 100644 --- a/infinigen/core/constraints/example_solver/room/utils.py +++ b/infinigen/core/constraints/example_solver/room/utils.py @@ -2,6 +2,9 @@ # This 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 From 991eb405d35aa806479304574f16b02104f8e867 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 224/727] Add 134 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/segment.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/segment.py 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..b7c69fbd8 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/segment.py @@ -0,0 +1,134 @@ +# Copyright (c) Princeton University. + +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 + unit_cast, update_exterior_edges, update_shared_edges, update_staircase_occupancies +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) + 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() From d88060b519102f99f2a4016f905748ca2524b428 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 225/727] Add 6 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/segment.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/segment.py b/infinigen/core/constraints/example_solver/room/segment.py index b7c69fbd8..8179bb4ee 100644 --- a/infinigen/core/constraints/example_solver/room/segment.py +++ b/infinigen/core/constraints/example_solver/room/segment.py @@ -1,4 +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. + from collections import defaultdict @@ -10,6 +13,7 @@ from infinigen.assets.utils.shapes import shared 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 @@ -38,6 +42,8 @@ def build_segments(self, staircase=None): 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, From 1b1c9f2fd2c7aa34a2df51b5b813747b9df6d784 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 226/727] Add 4 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/room/segment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/segment.py b/infinigen/core/constraints/example_solver/room/segment.py index 8179bb4ee..dc02f7962 100644 --- a/infinigen/core/constraints/example_solver/room/segment.py +++ b/infinigen/core/constraints/example_solver/room/segment.py @@ -2,6 +2,9 @@ # This 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 @@ -12,6 +15,7 @@ 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 eedf9ec534da2bdeda256ee2eb8227c2eca9452a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 227/727] Add 128 lines to infinigen/core/constraints/example_solver/room/contour.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../example_solver/room/contour.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/contour.py 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..74ffe80c3 --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/contour.py @@ -0,0 +1,128 @@ +# Copyright (c) Princeton University. +# This 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.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: + 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') From adac1356b697158313bd9008d8774dc68f552976 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 228/727] Add 4 lines to infinigen/core/constraints/example_solver/room/contour.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/room/contour.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/contour.py b/infinigen/core/constraints/example_solver/room/contour.py index 74ffe80c3..9b116c1d2 100644 --- a/infinigen/core/constraints/example_solver/room/contour.py +++ b/infinigen/core/constraints/example_solver/room/contour.py @@ -10,6 +10,9 @@ 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 @@ -21,6 +24,7 @@ @gin.configurable(denylist=['width', 'height']) class ContourFactory: + def __init__(self, width=17, height=9): self.width = width self.height = height self.n_trials = 1000 From 37ff78028b1bc0abc221828fc970ee75cd689cb5 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 229/727] Add 250 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../constraints/example_solver/room/scorer.py | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/room/scorer.py 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..626f2226d --- /dev/null +++ b/infinigen/core/constraints/example_solver/room/scorer.py @@ -0,0 +1,250 @@ +# Copyright (c) Princeton University. + +from collections import defaultdict + +import gin +import numpy as np +from shapely import LineString, Polygon + + TYPICAL_AREA_ROOM_TYPES + + +@gin.configurable(denylist=['graph']) +class BlueprintScorer: + 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(): + with np.errstate(invalid="ignore"): + 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) From cf7238b354bc4fc60b3dfc426491a73c82f9d33c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 230/727] Add 25 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/example_solver/room/scorer.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/scorer.py b/infinigen/core/constraints/example_solver/room/scorer.py index 626f2226d..9dbe25292 100644 --- a/infinigen/core/constraints/example_solver/room/scorer.py +++ b/infinigen/core/constraints/example_solver/room/scorer.py @@ -1,4 +1,7 @@ # Copyright (c) Princeton University. +# Authors: +# - Lingjie Mei: primary author +# - Karhan Kayan: fix constants from collections import defaultdict @@ -6,11 +9,33 @@ import numpy as np from shapely import LineString, Polygon +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 From b40d1e5cb3f01d6a075888e33e284ac5887d06c1 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 231/727] Add 6 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/example_solver/room/scorer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/core/constraints/example_solver/room/scorer.py b/infinigen/core/constraints/example_solver/room/scorer.py index 9dbe25292..c37eb1351 100644 --- a/infinigen/core/constraints/example_solver/room/scorer.py +++ b/infinigen/core/constraints/example_solver/room/scorer.py @@ -1,4 +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: primary author # - Karhan Kayan: fix constants @@ -9,6 +12,7 @@ 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 @@ -220,7 +224,9 @@ def functional_room(self, assignment, info): 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 + ( From fe82992f10925b287b6ece0a2f4121bff7be863a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 232/727] Add 113 lines to infinigen/core/constraints/example_solver/greedy/active_for_stage.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/greedy/active_for_stage.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/greedy/active_for_stage.py 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 From 5c34ab85ad40e83e5c8aba85b63f96a3329f7762 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:17 -0700 Subject: [PATCH 233/727] Add 269 lines to infinigen/core/constraints/example_solver/greedy/constraint_partition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../greedy/constraint_partition.py | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/greedy/constraint_partition.py 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 From 2559b726d5ca2c95927e4fb6cc9bc3cb0de0ed4b Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 234/727] Add 3 lines to infinigen/core/constraints/example_solver/greedy/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/core/constraints/example_solver/greedy/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/greedy/__init__.py 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 From c4ef4394684e1ba049a3abea9053fc675c21b8c1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 235/727] Add 174 lines to infinigen/core/constraints/example_solver/greedy/all_substitutions.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../greedy/all_substitutions.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/greedy/all_substitutions.py 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..75993596f --- /dev/null +++ b/infinigen/core/constraints/example_solver/greedy/all_substitutions.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: 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]: + + + 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=}') + + i = None + + 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=}') + + + From a7d99ffd126203cd08e865e39afc31437b42ddf9 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 236/727] Add 9 lines to infinigen/core/constraints/example_solver/greedy/all_substitutions.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../example_solver/greedy/all_substitutions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/core/constraints/example_solver/greedy/all_substitutions.py b/infinigen/core/constraints/example_solver/greedy/all_substitutions.py index 75993596f..c5237f390 100644 --- a/infinigen/core/constraints/example_solver/greedy/all_substitutions.py +++ b/infinigen/core/constraints/example_solver/greedy/all_substitutions.py @@ -65,6 +65,7 @@ def substitutions( 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 """ @@ -148,7 +149,15 @@ def iterate_assignments( 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]: From cef78964fe50dd2a14f90170abb47379001cc231 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 237/727] Add 266 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../example_solver/geometry/stability.py | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/geometry/stability.py 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..67e7b3acf --- /dev/null +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -0,0 +1,266 @@ +# Copyright (c) Princeton University. +# This 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 + +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 fcl + +# from infinigen.core.util import blender as butil +from infinigen.core.util import blender as butil +from mathutils import Vector, Quaternion +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 + + """ + check paralell, close to, and not overhanging. + """ + + logger.debug(f'stable against {obj_name=} {relation_state=}') + 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) + + 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 + + + mask = state.planes.tagged_plane_mask(sb.obj, mask, pb) + # Project mesh A onto the plane of mesh B + + + if projected_a is None or projected_b is None: + raise ValueError(f'Invalid {projected_a=} {projected_b=}') + + res = projected_a.within(projected_b.buffer(1e-2)) + 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) + return False + + for vertex in poly_a.vertices: + 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_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-05): + rotation_axis = rotation_axis / np.linalg.norm(rotation_axis) + else: + rotation_axis = np.array([0,0,1]) + + + + a_obj = bpy.data.objects[a] + a_poly = a_obj.data.polygons[a_poly_index] + # Recalculate vertex_a and normal_a after rotation + + 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 + + + +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. + """ + 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.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 + + random_point_global = random_sample_point(state, b_obj, face_mask, plane) + + +# 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] + + + if visualize: + fig, ax = plt.subplots() + ax.set_aspect('equal', 'box') + 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 + ) + 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 From 632f2ffd065c16143de6471cfea3c6144dfda2d8 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 238/727] Add 88 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/geometry/stability.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index 67e7b3acf..57d3cdaa3 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -19,11 +19,19 @@ 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.constraints.constraint_language import util as iu + import logging logger = logging.getLogger(__name__) @@ -73,35 +81,81 @@ def project_vector(vector, origin, normal): 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() # 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 @@ -122,10 +176,18 @@ def snap_against(scene, a, b, a_plane, b_plane, margin = 0): 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): @@ -133,15 +195,26 @@ def snap_against(scene, a, b, a_plane, b_plane, margin = 0): 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) + @@ -149,6 +222,10 @@ def random_sample_point(state: state_def.State, obj: bpy.types.Object, face_mask """ 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( @@ -157,6 +234,7 @@ def random_sample_point(state: state_def.State, obj: bpy.types.Object, face_mask # 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]] @@ -189,8 +267,10 @@ def move_obj_random_pt(state: state_def.State, a, b, face_mask: np.ndarray, plan 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): @@ -226,10 +306,15 @@ def supported_by(scene, a, b, visualize = False): 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') @@ -245,6 +330,9 @@ def supported_by(scene, a, b, visualize = 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: From 72dff47682098cac7f8961b9cad115bbd6f0ece1 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 239/727] Add 4 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../core/constraints/example_solver/geometry/stability.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index 57d3cdaa3..c6a5c3b4a 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -9,6 +9,7 @@ from copy import copy import numpy as np +from shapely.affinity import rotate import bpy import trimesh @@ -30,6 +31,7 @@ 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 @@ -124,6 +126,8 @@ def stable_against( 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) From 32f8dd7fc273017fcff17d35c07a65eab99f85b1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 240/727] Add 67 lines to infinigen/core/constraints/example_solver/geometry/validity.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/geometry/validity.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/geometry/validity.py 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..a2788a9e4 --- /dev/null +++ b/infinigen/core/constraints/example_solver/geometry/validity.py @@ -0,0 +1,67 @@ +import logging + +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.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 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 +): + + 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: + + 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)=}') From a475584445c6258c4adc2b2bf4119861778654bb Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 241/727] Add 55 lines to infinigen/core/constraints/example_solver/geometry/validity.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../example_solver/geometry/validity.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/validity.py b/infinigen/core/constraints/example_solver/geometry/validity.py index a2788a9e4..c0761647f 100644 --- a/infinigen/core/constraints/example_solver/geometry/validity.py +++ b/infinigen/core/constraints/example_solver/geometry/validity.py @@ -1,18 +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: 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 @@ -36,6 +84,7 @@ def check_post_move_validity( visualize=False ): + scene = state.trimesh_scene objstate = state.objs[name] collision_objs = [ @@ -44,8 +93,10 @@ def check_post_move_validity( ] 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}' @@ -65,3 +116,7 @@ def check_post_move_validity( 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 From 952f47f0259dbed6fc6469efef6ba5cdc405b079 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 242/727] Add 268 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../example_solver/geometry/planes.py | 268 ++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/geometry/planes.py 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..6b73a66cc --- /dev/null +++ b/infinigen/core/constraints/example_solver/geometry/planes.py @@ -0,0 +1,268 @@ +# Copyright (c) Princeton University. +# This 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 +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 + +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 + """ + if not mask.any(): + 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 + + + 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): + parent_plane = None + else: + parent_plane = parent_all_planes[relation_state.parent_plane_idx] + + if relation_state.child_plane_idx >= len(obj_all_planes): + 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!") + + mask = self.tagged_plane_mask(obj, face_mask, plane) + + if not mask.any(): + 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] + 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 + + From 273125b205f9625f8a57ebbf84c670f35a3fe2cb Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 243/727] Add 27 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/geometry/planes.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/planes.py b/infinigen/core/constraints/example_solver/geometry/planes.py index 6b73a66cc..2a88bbfa9 100644 --- a/infinigen/core/constraints/example_solver/geometry/planes.py +++ b/infinigen/core/constraints/example_solver/geometry/planes.py @@ -4,6 +4,17 @@ # Authors: Karhan Kayan from __future__ import annotations +import logging + +import bpy +import numpy as np +import gin + +import infinigen.core.util.blender as butil +from infinigen.core import tagging, tags as t + +logger = logging.getLogger(__name__) + def global_vertex_coordinates(obj, local_vertex): return obj.matrix_world @ local_vertex.co @@ -11,6 +22,10 @@ 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): @@ -54,6 +69,7 @@ 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 @@ -113,17 +129,22 @@ 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) + if not mask.any(): 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 @@ -144,11 +165,13 @@ def get_tagged_planes(self, obj: bpy.types.Object, tags: set, fast=True): #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] @@ -216,11 +239,14 @@ def tagged_plane_mask(self, obj: bpy.types.Object, face_mask: np.ndarray, 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 @@ -266,3 +292,4 @@ def _compute_tagged_plane_mask(self, obj, face_mask, plane, tolerance): plane_mask[candidate_polygon.index] = in_plane + return plane_mask \ No newline at end of file From a62d5e6899ea516b649ab7b3ed759f8eeecdb361 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 244/727] Add 8 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../core/constraints/example_solver/geometry/planes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/planes.py b/infinigen/core/constraints/example_solver/geometry/planes.py index 2a88bbfa9..5e5cd1f9c 100644 --- a/infinigen/core/constraints/example_solver/geometry/planes.py +++ b/infinigen/core/constraints/example_solver/geometry/planes.py @@ -9,9 +9,12 @@ 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__) @@ -132,7 +135,9 @@ def get_tagged_planes(self, obj: bpy.types.Object, tags: set, fast=True): 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=}' ) @@ -191,9 +196,11 @@ def extract_tagged_plane(self, obj: bpy.types.Object, tags: set, plane: int): 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=}' ) @@ -221,6 +228,7 @@ def extract_tagged_plane(self, obj: bpy.types.Object, tags: set, plane: int): 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) From 570636af3cf3c25f722a79a917ee8f69095f9ca7 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 245/727] Add 259 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../example_solver/geometry/dof.py | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/geometry/dof.py 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..335d319f1 --- /dev/null +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -0,0 +1,259 @@ +# Copyright (c) Princeton University. +# This 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 numpy as np +import infinigen.core.util.blender as butil +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.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS + + + + +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 + + """ + Given a list of relations (each a tuple of point and normal), + compute the combined 3x3 matrix M. + """ + M = np.identity(3) + 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 + + """ + Given a list of normals, compute the combined axis of rotation. + If there are conflicting constraints, return None. + """ + # 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 + 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 get_rot(ind): + 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) + np.allclose(rotation_axis, reference_normal, atol=1e-02) + or np.allclose(rotation_axis, -reference_normal, atol=1e-02) + + + 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) + + + # 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: + 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: + 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 + + obj_state = state.objs[name] + obj_name = obj_state.obj.name + parent_objs = [] + parent_planes = [] + obj_planes = [] + margins = [] + parent_tag_list = [] + 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 + obj_planes.append(obj_plane) + parent_planes.append(parent_plane) + parent_objs.append(parent_obj) + match relation_state.relation: + margins.append(margin) + parent_tag_list.append(parent_tags) + margins.append(0) + parent_tag_list.append(parent_tags) + case _: + raise NotImplementedError + if not valid: + return None + if dof == 0: + iu.translate(state.trimesh_scene, obj_name, T) + elif dof == 1: + 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 all([p1_to_p1.buffer(1e-1).contains(Point(pt[0], pt[1])) for pt in projected]): + 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: + 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: + 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)) + stability.move_obj_random_pt(state, obj_name, parent_obj.name, face_mask, parent_plane) + match relation_state.relation: + stability.snap_against(state.trimesh_scene, obj_name, parent_obj.name, obj_plane, parent_plane, margin=margin) + stability.snap_against(state.trimesh_scene, obj_name, parent_obj.name, obj_plane, parent_plane, margin=0) + case _: + raise NotImplementedError + 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: + 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) + vis.name = obj_state.obj.name[:30] + '_failure_' + str(retry) + # butil.save_blend("test.blend") + + From c1cd289f03774e62b4482664dd4f233d34cfef54 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 246/727] Add 150 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../example_solver/geometry/dof.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 335d319f1..0f93ea477 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -3,15 +3,28 @@ # Authors: Karhan Kayan +import logging +import gin import numpy as np +from infinigen.core.constraints.example_solver.geometry import stability + +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.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS +logger = logging.getLogger(__name__) def stable_against_matrix(point, normal): """ @@ -25,11 +38,18 @@ def stable_against_matrix(point, normal): 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 @@ -42,15 +62,23 @@ def rotation_constraint(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 @@ -69,7 +97,26 @@ def rotate_object_around_axis(obj, axis, std, angle=None): 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] @@ -96,10 +143,17 @@ def get_rot(ind): 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 @@ -107,8 +161,22 @@ def is_rotation_allowed(rotation_axis, reference_normal): # 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 = [] @@ -152,11 +220,13 @@ def is_rotation_allowed(rotation_axis, reference_normal): 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 @@ -165,14 +235,29 @@ def project(points, 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) @@ -180,21 +265,32 @@ def project(points, plane_normal): 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] @@ -205,14 +301,19 @@ def project(points, plane_normal): 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=}') @@ -226,6 +327,7 @@ def project(points, plane_normal): 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) @@ -236,10 +338,45 @@ def project(points, plane_normal): iu.set_rotation(state.trimesh_scene, obj_name, (0, 0, 2*np.pi*np.random.randint(0, 4)/4)) 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=}") @@ -248,12 +385,25 @@ def project(points, plane_normal): # 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 + + From fccec0a26b27fb84a612a0de0140f6a053d1d611 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 247/727] Add 10 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../core/constraints/example_solver/geometry/dof.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 0f93ea477..4f3554651 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -6,8 +6,10 @@ import logging import gin +import bpy import numpy as np from infinigen.core.constraints.example_solver.geometry import stability +from mathutils import Vector from infinigen.core import tags as t, tagging from infinigen.core.constraints import constraint_language as cl @@ -34,6 +36,7 @@ def stable_against_matrix(point, normal): # 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 @@ -60,6 +63,7 @@ def rotation_constraint(normal): # 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): @@ -75,12 +79,16 @@ def combine_rotation_constraints(parent_planes, eps=0.01): # 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 @@ -91,9 +99,11 @@ def rotate_object_around_axis(obj, axis, std, angle=None): # 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)) From 73a094d4fc1cb19c0fb9eee17b01729810f3f9c9 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 248/727] Add 7 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/example_solver/geometry/dof.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 4f3554651..5f60e1e43 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -10,6 +10,9 @@ 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 @@ -22,6 +25,7 @@ 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 @@ -328,10 +332,12 @@ def apply_relations_surfacesample( 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) @@ -346,6 +352,7 @@ def apply_relations_surfacesample( 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): From 05ada8da1d63a11248e2960b32e1a5760c2cc13f Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 249/727] Add 68 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../example_solver/geometry/parse_scene.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 infinigen/core/constraints/example_solver/geometry/parse_scene.py 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..ddcbd3187 --- /dev/null +++ b/infinigen/core/constraints/example_solver/geometry/parse_scene.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: Karhan Kayan + +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.constraints.constraint_language.util import ( + translate, + rotate, + sync_trimesh +) +import fcl + +def to_trimesh(obj: bpy.types.Object): + 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) + + + + # convert all bpy.objects into a trimesh.Scene + + scene = trimesh.Scene() + + 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) + 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 From 003d26335d8f020d07b8dc42446c2003dcb88dc2 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 250/727] Add 7 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../constraints/example_solver/geometry/parse_scene.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/parse_scene.py b/infinigen/core/constraints/example_solver/geometry/parse_scene.py index ddcbd3187..e31dd17c2 100644 --- a/infinigen/core/constraints/example_solver/geometry/parse_scene.py +++ b/infinigen/core/constraints/example_solver/geometry/parse_scene.py @@ -9,7 +9,9 @@ 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, @@ -41,9 +43,14 @@ def preprocess_scene(objects): +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 From d6a10735306ab8ed7fd8c2a3a19f3dbeb72d006d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 251/727] Add 3 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../core/constraints/example_solver/geometry/parse_scene.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/example_solver/geometry/parse_scene.py b/infinigen/core/constraints/example_solver/geometry/parse_scene.py index e31dd17c2..ff32de34e 100644 --- a/infinigen/core/constraints/example_solver/geometry/parse_scene.py +++ b/infinigen/core/constraints/example_solver/geometry/parse_scene.py @@ -3,6 +3,7 @@ # Authors: Karhan Kayan +import bpy import trimesh from shapely import LineString, Point import numpy as np @@ -60,11 +61,13 @@ def add_to_scene(scene, obj, preprocess=True): 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]) From 8a33c018890f2c57d0cacd67cb48ca00bb037322 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 252/727] Add 1 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../core/constraints/example_solver/geometry/parse_scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/example_solver/geometry/parse_scene.py b/infinigen/core/constraints/example_solver/geometry/parse_scene.py index ff32de34e..11cfd8cf1 100644 --- a/infinigen/core/constraints/example_solver/geometry/parse_scene.py +++ b/infinigen/core/constraints/example_solver/geometry/parse_scene.py @@ -21,6 +21,7 @@ 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) From 7bd06495540dfcd31b0cfa273cb0762da96cabab Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 253/727] Add 66 lines to infinigen/core/constraints/constraint_language/gather.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/constraint_language/gather.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/gather.py diff --git a/infinigen/core/constraints/constraint_language/gather.py b/infinigen/core/constraints/constraint_language/gather.py new file mode 100644 index 000000000..49344dea1 --- /dev/null +++ b/infinigen/core/constraints/constraint_language/gather.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 + +import typing +from dataclasses import dataclass, field + +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 From eb95bd3d8b874e8b8096042798b557cd2e8654bd Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 254/727] Add 1 lines to infinigen/core/constraints/constraint_language/gather.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/constraint_language/gather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/constraint_language/gather.py b/infinigen/core/constraints/constraint_language/gather.py index 49344dea1..071516d19 100644 --- a/infinigen/core/constraints/constraint_language/gather.py +++ b/infinigen/core/constraints/constraint_language/gather.py @@ -7,6 +7,7 @@ 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 From a5d7f549cfd7ce22ee6241ca9be116343cc7173a Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 255/727] Add 354 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../constraints/constraint_language/util.py | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/util.py diff --git a/infinigen/core/constraints/constraint_language/util.py b/infinigen/core/constraints/constraint_language/util.py new file mode 100644 index 000000000..0760e50f5 --- /dev/null +++ b/infinigen/core/constraints/constraint_language/util.py @@ -0,0 +1,354 @@ +# Copyright (c) Princeton University. +# This 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, Polygon, MultiPolygon +import numpy as np +from infinigen.core.util import blender as butil + + +@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 + + 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') + + + 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 + + + if isinstance(names, str): + names = [names] + + col = trimesh.collision.CollisionManager() + for name in names: + T, g = scene.graph[name] + if tags is not None and len(tags) > 0: + 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) + # col.add_object(name, geom, T) + add_object_cached(col, name, geom.col_obj, geom.fcl_obj) + if bvh_cache is not None and bvh_caching_config(): + 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] + From c0ac20054ffc6e122aa2c0dc24f1a9b4973ce6c4 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 256/727] Add 46 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/constraint_language/util.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/util.py b/infinigen/core/constraints/constraint_language/util.py index 0760e50f5..36bf7affa 100644 --- a/infinigen/core/constraints/constraint_language/util.py +++ b/infinigen/core/constraints/constraint_language/util.py @@ -2,13 +2,29 @@ # This 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 mathutils import Matrix, Vector +import gin + from infinigen.core.util import blender as butil +logger = logging.getLogger(__name__) + +@gin.configurable +def bvh_caching_config(enabled=True): + return enabled @functools.cache def group(scene, x): @@ -167,8 +183,12 @@ def sample_random_point(polygon): 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: @@ -176,6 +196,7 @@ def sample_random_point(polygon): 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): @@ -260,21 +281,46 @@ def add_object_cached(col, 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'): From 2dc96951524da134bc426e4129b4baf8dc998859 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 257/727] Add 4 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/constraint_language/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/util.py b/infinigen/core/constraints/constraint_language/util.py index 36bf7affa..8d5d0bffc 100644 --- a/infinigen/core/constraints/constraint_language/util.py +++ b/infinigen/core/constraints/constraint_language/util.py @@ -14,11 +14,15 @@ 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__) From fca9314c981a5a3644fc84b0611dea30215642c1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 258/727] Add 363 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraint_language/relations.py | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/relations.py diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py new file mode 100644 index 000000000..99957e956 --- /dev/null +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -0,0 +1,363 @@ +# This 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 abc import ABC, abstractmethod +from dataclasses import dataclass, field, fields +from copy import deepcopy +import logging + +from infinigen.core.constraints import constraint_language as cl + +logger = logging.getLogger(__name__) +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 + 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 + + 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 __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) + ) + + 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) + ) + + + 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 + + def intersection(self, other: Relation) -> Relation: + return self.rel.difference(other) + + def difference(self, other: Relation) -> Relation: + return self.rel.intersection(other) + + 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)})' + + __repr__ = no_frozenset_repr + + # use object.__setattr__ to bypass dataclass's frozen since it is guaranteed safe here + 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: + + 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 + logger.debug(f'{self.intersects} failed child tags for other=%s', other) + return False + 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() + +class Touching(GeometryRelation): + __repr__ = no_frozenset_repr + + +@dataclass(frozen=True) +class SupportedBy(Touching): + __repr__ = no_frozenset_repr + + + # 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 + + + + 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() From 450843326d7921f5df6b989075bd42931e9d6cff Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:18 -0700 Subject: [PATCH 259/727] Add 37 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../constraint_language/relations.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index 99957e956..6d0d136f1 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -2,15 +2,20 @@ # 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 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: @@ -27,7 +32,9 @@ def satisfies(self, other: Relation) -> bool: pass @abstractmethod + def intersects(self, other, strict=False) -> bool: pass + @abstractmethod def intersection(self, other: Relation) -> Relation: pass @@ -48,6 +55,7 @@ def implies(self, other) -> bool: 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: @@ -76,6 +84,7 @@ def implies(self, other: Relation) -> bool: case _: return ( not self.rel.implies(other) + and not self.intersects(other, strict=True) ) def satisfies(self, other: cl.Relation) -> bool: @@ -90,6 +99,7 @@ def satisfies(self, other: cl.Relation) -> bool: and not self.intersects(other, strict=True) ) + def intersects(self, other: Relation, strict=False) -> bool: match other: case NegatedRelation(rel): @@ -99,6 +109,7 @@ def satisfies(self, other: cl.Relation) -> bool: 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) @@ -106,6 +117,9 @@ def intersection(self, other: Relation) -> Relation: def difference(self, other: Relation) -> Relation: return self.rel.intersection(other) +class ConnectorType(Enum): + Door = "door" +@dataclass(frozen=True) connector_types: frozenset[ConnectorType] = field(default_factory=frozenset) def __post_init__(self): if self.connector_types is not None: @@ -159,9 +173,19 @@ def no_frozenset_repr(self: GeometryRelation): 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. @@ -247,6 +271,12 @@ def satisfies(self, other: Relation) -> bool: 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) @@ -259,8 +289,10 @@ def intersects(self, other: Relation, strict=False) -> bool: 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 @@ -320,6 +352,7 @@ def difference(self: Relation, other: Relation) -> Relation: logger.warning(f'Encountered unhandled {other=} for {self.intersection}') return -AnyRelation() +@dataclass(frozen=True) class Touching(GeometryRelation): __repr__ = no_frozenset_repr @@ -328,6 +361,10 @@ class Touching(GeometryRelation): 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 From e36f911cb00fdde485fa8618b0bd35f82a251fa0 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 260/727] Add 10 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../core/constraints/constraint_language/relations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index 6d0d136f1..88bd82705 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.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 + from __future__ import annotations from abc import ABC, abstractmethod @@ -17,6 +19,7 @@ @dataclass(frozen=True) class Relation(ABC): + @abstractmethod def implies(self, other) -> bool: """ @@ -119,8 +122,13 @@ def difference(self, other: Relation) -> Relation: 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)) @@ -377,6 +385,8 @@ class StableAgainst(GeometryRelation): __repr__ = no_frozenset_repr +@dataclass(frozen=True) +class CutFrom(Relation): def implies(self, other: Relation) -> bool: return ( From e2107e4646e124050894b35bc96743103d6e0cbe Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 261/727] Add 3 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/core/constraints/constraint_language/relations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index 88bd82705..d9db1d613 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -75,6 +75,9 @@ class NegatedRelation(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 From bebd70235a1d7b900e6cde2991537b09bf74baa0 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 262/727] Add 1 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/core/constraints/constraint_language/relations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index d9db1d613..74483d0b0 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -9,6 +9,7 @@ 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 7b5d07df0c73e4b0f8559d5b44c07c107a8768b6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 263/727] Add 54 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraint_language/__init__.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/__init__.py diff --git a/infinigen/core/constraints/constraint_language/__init__.py b/infinigen/core/constraints/constraint_language/__init__.py new file mode 100644 index 000000000..a7771620e --- /dev/null +++ b/infinigen/core/constraints/constraint_language/__init__.py @@ -0,0 +1,54 @@ + +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, + freespace_2d, + min_dist_2d, + center_stable_surface_dist, + accessibility_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 From c194abfaeb508689422514c5231704e91116ec44 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 264/727] Add 11 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../core/constraints/constraint_language/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/__init__.py b/infinigen/core/constraints/constraint_language/__init__.py index a7771620e..c5bf9c62a 100644 --- a/infinigen/core/constraints/constraint_language/__init__.py +++ b/infinigen/core/constraints/constraint_language/__init__.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, Karhan Kayan + from .expression import ( Expression, @@ -25,10 +31,15 @@ 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 ( From feb01a15d3d865c1199e2c5d00d465462d575123 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 265/727] Add 2 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/constraint_language/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/__init__.py b/infinigen/core/constraints/constraint_language/__init__.py index c5bf9c62a..5b386e1cc 100644 --- a/infinigen/core/constraints/constraint_language/__init__.py +++ b/infinigen/core/constraints/constraint_language/__init__.py @@ -4,6 +4,8 @@ # Authors: Alexander Raistrick, Karhan Kayan +from infinigen.core.tags import Semantics, Negated +from .types import Node from .expression import ( Expression, From 2ff07440243d0dee94036800fefa596095185a7b Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 266/727] Add 42 lines to infinigen/core/constraints/constraint_language/types.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/constraint_language/types.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/types.py diff --git a/infinigen/core/constraints/constraint_language/types.py b/infinigen/core/constraints/constraint_language/types.py new file mode 100644 index 000000000..b2c38866c --- /dev/null +++ b/infinigen/core/constraints/constraint_language/types.py @@ -0,0 +1,42 @@ +# Copyright (c) Princeton University. +# This 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 From 5795a6a8922f490acd140b6937e3b85c377c4662 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 267/727] Add 3 lines to infinigen/core/constraints/constraint_language/types.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/constraint_language/types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/types.py b/infinigen/core/constraints/constraint_language/types.py index b2c38866c..d6e07686d 100644 --- a/infinigen/core/constraints/constraint_language/types.py +++ b/infinigen/core/constraints/constraint_language/types.py @@ -40,3 +40,6 @@ def traverse(self, inorder=True): yield from c.traverse(inorder=inorder) if not inorder: yield self + + def size(self): + return len(list(self.traverse())) From 8a766fdd41d2853ca918c90fd8f1e8e11c95fe2f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 268/727] Add 32 lines to infinigen/core/constraints/constraint_language/result.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraints/constraint_language/result.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/result.py 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 From 184cc566002df0c53e6bd9d4e4136fb74ac63700 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 269/727] Add 80 lines to infinigen/core/constraints/constraint_language/set_reasoning.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraint_language/set_reasoning.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/set_reasoning.py 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..7473dd90d --- /dev/null +++ b/infinigen/core/constraints/constraint_language/set_reasoning.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: Alexander Raistrick + +import typing +from dataclasses import dataclass, field + +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 + + 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) + + +@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') + From 5ecc47c1f9505e5612fe018ea0df2516b1130474 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 270/727] Add 3 lines to infinigen/core/constraints/constraint_language/set_reasoning.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- .../core/constraints/constraint_language/set_reasoning.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/set_reasoning.py b/infinigen/core/constraints/constraint_language/set_reasoning.py index 7473dd90d..b87df0e36 100644 --- a/infinigen/core/constraints/constraint_language/set_reasoning.py +++ b/infinigen/core/constraints/constraint_language/set_reasoning.py @@ -7,6 +7,7 @@ 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 @@ -25,6 +26,7 @@ class scene(ObjectSetExpression): @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) @@ -37,6 +39,7 @@ def excludes(objs, tags): if isinstance(objs, tagged): tags = tags.difference(objs.tags) + return tagged(objs, {t.Negated(x) for x in tags}) @ObjectSetExpression.register_postfix_func @nodedataclass() From ddebf5a9ae03614aa9a6df09dba1f77ddc34cd23 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 271/727] Add 55 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraint_language/geometry.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/geometry.py diff --git a/infinigen/core/constraints/constraint_language/geometry.py b/infinigen/core/constraints/constraint_language/geometry.py new file mode 100644 index 000000000..c0a1d74a9 --- /dev/null +++ b/infinigen/core/constraints/constraint_language/geometry.py @@ -0,0 +1,55 @@ +import typing +from dataclasses import dataclass, field + +import numpy as np + +from .expression import Expression, BoolExpression, ScalarExpression, nodedataclass +from .set_reasoning import ObjectSetExpression +class center_stable_surface_dist(ScalarExpression): + 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() + objs: ObjectSetExpression + others: ObjectSetExpression +@nodedataclass() + objs: ObjectSetExpression + others: ObjectSetExpression + 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() + objs: ObjectSetExpression + others: ObjectSetExpression +@nodedataclass() + objs: ObjectSetExpression + others: ObjectSetExpression +@nodedataclass() + objs: ObjectSetExpression + +@ObjectSetExpression.register_postfix_func +@nodedataclass() +class volume(ScalarExpression): + objs: ObjectSetExpression From 243c6dd8457b47c8c3b6efee9e7881181b71a295 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 272/727] Add 35 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- .../constraint_language/geometry.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/geometry.py b/infinigen/core/constraints/constraint_language/geometry.py index c0a1d74a9..abff2076d 100644 --- a/infinigen/core/constraints/constraint_language/geometry.py +++ b/infinigen/core/constraints/constraint_language/geometry.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 typing from dataclasses import dataclass, field @@ -5,9 +10,18 @@ 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) @@ -30,26 +44,47 @@ 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 From aea06966bf909dc428034c13c62b6104f3d9530f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 273/727] Add 2 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/core/constraints/constraint_language/geometry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/core/constraints/constraint_language/geometry.py b/infinigen/core/constraints/constraint_language/geometry.py index abff2076d..2e4fd8851 100644 --- a/infinigen/core/constraints/constraint_language/geometry.py +++ b/infinigen/core/constraints/constraint_language/geometry.py @@ -8,6 +8,8 @@ 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 From fbdf2b1e411348fbc22f0a026b3da1375570b003 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 274/727] Add 182 lines to infinigen/core/constraints/constraint_language/expression.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../constraint_language/expression.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 infinigen/core/constraints/constraint_language/expression.py 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 From 0f6370fc2ddac694a6ccace374c8153a51be5abd Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 275/727] Add 79 lines to infinigen/core/util/bevelling.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/core/util/bevelling.py | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 infinigen/core/util/bevelling.py 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))})}) From 9bc9ebdd7bad0f2d33fa5590b495caf00f90d5f1 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 276/727] Add 53 lines to infinigen/core/nodes/shader_utils.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/core/nodes/shader_utils.py | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 infinigen/core/nodes/shader_utils.py 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 From 64642562e634122ac400f1c0bcffebccfbbd63f9 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 277/727] Add 173 lines to infinigen/core/placement/path_finding.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/core/placement/path_finding.py | 173 +++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 infinigen/core/placement/path_finding.py 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 From 8239967d6d61df91b0c6e6f54d810010324b467d Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 278/727] Add 93 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/color_fits.py | 93 ++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 infinigen/assets/color_fits.py diff --git a/infinigen/assets/color_fits.py b/infinigen/assets/color_fits.py new file mode 100644 index 000000000..cfc18fd7f --- /dev/null +++ b/infinigen/assets/color_fits.py @@ -0,0 +1,93 @@ +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 From 474a9f049252d5240f0a23830d2da3647f2fa9bc Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 279/727] Add 4 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/color_fits.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/color_fits.py b/infinigen/assets/color_fits.py index cfc18fd7f..e3e752659 100644 --- a/infinigen/assets/color_fits.py +++ b/infinigen/assets/color_fits.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. + + import os import numpy as np from matplotlib import pyplot as plt From 3ccec42b61797e060978a77c1bc4aa4b32add210 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 280/727] Add 1 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/color_fits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/color_fits.py b/infinigen/assets/color_fits.py index e3e752659..a04a3b40f 100644 --- a/infinigen/assets/color_fits.py +++ b/infinigen/assets/color_fits.py @@ -1,6 +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: Stamatis Alexandropoulos, Meenal Parakh import os import numpy as np From f79563117d78bc23849874a5d698e9afd399a3ab Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 281/727] Add 456 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/material_assignments.py | 456 +++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 infinigen/assets/material_assignments.py diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py new file mode 100644 index 000000000..27f6faf5f --- /dev/null +++ b/infinigen/assets/material_assignments.py @@ -0,0 +1,456 @@ +# Copyright (c) Princeton University. +# This 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 infinigen.assets.materials import (metal, plastic, text, ceramic, woods, dirt, + mirror, +from infinigen.assets.materials import (beverage_fridge_shaders, dishwasher_shaders, + ceiling_light_shaders, + vase_shaders, + lamp_shaders, table_marble, + 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 + + +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(): + return new_shader + + +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 { + "screen_surface": TextureAssignments([text.Text], [1.0]), + "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]), + "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 { + "wear_tear": [procedural_scratch, procedural_edge_wear], + "wear_tear_prob": [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB] + } + +def blanket_materials(): + return { + [1.0, 1.0]), +def pants_materials(): + return { + } +def towel_materials(): + return { + [0.2, 0.8]), + } + +def acquarium_materials(): + return { + "glass_surface": TextureAssignments([glass], [1.0]), + "water_surface": TextureAssignments([water], [1.0]), + "wear_tear": [procedural_scratch, procedural_edge_wear], + } +def ceiling_light_materials(): + return { + "black_material": TextureAssignments([ceiling_light_shaders.shader_black], [1.0]), + "wear_tear": [procedural_scratch, procedural_edge_wear], +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], + } + +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 { + [1.0, 1.0, 1.0, 1.0]), + 'leg': TextureAssignments([*metal_shaders, + [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() + probs = [1.0 / len(metal_shaders)] * len(metal_shaders) + + return { + 'top': MaterialOptions([ + (table_marble.shader_marble, 2.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([ + (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 { + [1.0]), + [1.0] + probs), + "wear_tear": [procedural_scratch, procedural_edge_wear], + } + +def chair_materials(): + return { + "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, + glass_volume.shader_glass_volume], + [1.0, 1.0, 1.0, 1.0]), + [1.0] + probs), + "wear_tear": [procedural_scratch, procedural_edge_wear], + 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, DEFAULT_EDGE_WEAR_PROB] + } + +def bedframe_materials(): + return { + "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], + } + +def vase_materials(): + return { + [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 { + # no guard as it overrides over tableware_materials + } +def cup_materials(): + return { + 'surface': TextureAssignments([glass, plastic], [1.0, 1.0]), + } +def bottle_materials(): + return { + "surface": TextureAssignments([glass, plastic], [1.0, 1.0]), + "wrap_surface": TextureAssignments([text.Text], [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: + return { + "surface": surface_materials, + "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]), + "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 { + "wear_tear": [procedural_scratch, procedural_edge_wear], + 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0] + } +def glasslid_materials(): + return { + "surface": TextureAssignments([glass], [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 { + [1.0]), + } + +def range_hood_materials(): + return { + } + +def wall_art_materials(): + return { + "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 { + "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_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() + 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] + +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 + 'BathroomSinkFactory': bathroom_sink_materials, # inheriting from bathtub factory, so not used + 'HardwareFactory': hardware_materials, + 'ToiletFactory': toilet_materials, + # clothes + # also "Normal Not Found" is printed when generating + ############## 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 + '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 + 'PotFactory': None, # uses the same materials as PanFactory + 'CupFactory': cup_materials, + 'WineglassFactory': None, # uses materials from transparent tableware + '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, From 492aab6eae187ef9bd66dc16b8c1da3f0bf22dca Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 282/727] Add 119 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/material_assignments.py | 119 +++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py index 27f6faf5f..53311c565 100644 --- a/infinigen/assets/material_assignments.py +++ b/infinigen/assets/material_assignments.py @@ -7,12 +7,20 @@ 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 (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 @@ -20,12 +28,15 @@ 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() @@ -58,6 +69,11 @@ def plastic_furniture(): 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 { @@ -106,16 +122,22 @@ def oven_materials(): 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]), @@ -129,39 +151,54 @@ def toilet_materials(): "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]), "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(): @@ -169,8 +206,14 @@ def table_cocktail_materials(): # 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] @@ -178,6 +221,7 @@ def table_cocktail_materials(): 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 { @@ -197,6 +241,7 @@ def table_dining_materials(): "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": @@ -204,16 +249,27 @@ def bar_chair_materials(leg_style=None): 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], } 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": @@ -223,8 +279,12 @@ def office_chair_materials(leg_style=None): 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] @@ -232,19 +292,25 @@ def office_chair_materials(leg_style=None): 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([ @@ -253,33 +319,45 @@ def sofa_materials(): (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: @@ -287,12 +365,17 @@ def tableware_materials(fragile=False, transparent=False): 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]), @@ -304,9 +387,11 @@ def can_materials(): 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]), @@ -314,12 +399,17 @@ def foodbag_materials(): 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] } @@ -332,27 +422,41 @@ def plant_container_materials(): 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 { @@ -360,13 +464,21 @@ def kitchen_sink_materials(): "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] + } AssetList = { # appliances @@ -377,11 +489,14 @@ def kitchen_tap_materials(): '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 @@ -426,6 +541,7 @@ def kitchen_tap_materials(): '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 @@ -433,9 +549,11 @@ def kitchen_tap_materials(): '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, @@ -454,3 +572,4 @@ def kitchen_tap_materials(): # window 'WindowFactory': None, "RugFactory": rug_materials, +} From a0672a069f40b110ebf9e0ab32686b64eb3247cc Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 283/727] Add 15 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/material_assignments.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py index 53311c565..cd98db652 100644 --- a/infinigen/assets/material_assignments.py +++ b/infinigen/assets/material_assignments.py @@ -16,6 +16,7 @@ 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, from infinigen.assets.materials import (beverage_fridge_shaders, dishwasher_shaders, ceiling_light_shaders, vase_shaders, @@ -66,6 +67,8 @@ def get_all_metal_shaders(): 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 @@ -189,6 +192,7 @@ def acquarium_materials(): 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] } @@ -227,6 +231,7 @@ def table_dining_materials(): 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), @@ -234,6 +239,7 @@ def table_dining_materials(): *(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)), @@ -480,6 +486,15 @@ def kitchen_tap_materials(): "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 From 080c3a5952a01b9b88f058c3fd43dffa41ba2b11 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 284/727] Add 1 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/material_assignments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py index cd98db652..4151c3e6c 100644 --- a/infinigen/assets/material_assignments.py +++ b/infinigen/assets/material_assignments.py @@ -17,6 +17,7 @@ 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, From e920cc30017f94e92ea0af15d3d876dddb068025 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 285/727] Add 1 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/material_assignments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/material_assignments.py b/infinigen/assets/material_assignments.py index 4151c3e6c..a11d1bdff 100644 --- a/infinigen/assets/material_assignments.py +++ b/infinigen/assets/material_assignments.py @@ -262,6 +262,7 @@ def bar_chair_materials(leg_style=None): *metal_shaders], [1.0] + probs), "wear_tear": [procedural_scratch, procedural_edge_wear], + 'wear_tear_prob': [DEFAULT_SCRATCH_PROB, 0.0] } def chair_materials(): From 3171227dcba6ed7d3afb4e39625cf4f3f3e801fe Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 286/727] Add 67 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/fruit_container.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 infinigen/assets/tableware/fruit_container.py diff --git a/infinigen/assets/tableware/fruit_container.py b/infinigen/assets/tableware/fruit_container.py new file mode 100644 index 000000000..2cfca8c25 --- /dev/null +++ b/infinigen/assets/tableware/fruit_container.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 +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, + + +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) + + 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') From d49acf85887b999beb3991b3fcdfcf31aff82e11 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:19 -0700 Subject: [PATCH 287/727] Add 2 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/tableware/fruit_container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/tableware/fruit_container.py b/infinigen/assets/tableware/fruit_container.py index 2cfca8c25..4dfa47a75 100644 --- a/infinigen/assets/tableware/fruit_container.py +++ b/infinigen/assets/tableware/fruit_container.py @@ -36,6 +36,8 @@ def apply(self, obj, selection=None): 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): From bc7c9c9480dbb8cb66d37eede39ea472500c9c28 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 288/727] Add 2 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/fruit_container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/tableware/fruit_container.py b/infinigen/assets/tableware/fruit_container.py index 4dfa47a75..29b5638e1 100644 --- a/infinigen/assets/tableware/fruit_container.py +++ b/infinigen/assets/tableware/fruit_container.py @@ -54,6 +54,7 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -61,6 +62,7 @@ def cover(self): 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) From 58da17cb07aa37be65a7431889eec54f6bc5c82f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 289/727] Add 81 lines to infinigen/assets/tableware/chopsticks.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/chopsticks.py | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 infinigen/assets/tableware/chopsticks.py 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 From b171c0fa84cae7ce11f626abcb95b8f738465a8a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 290/727] Add 94 lines to infinigen/assets/tableware/knife.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/knife.py | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 infinigen/assets/tableware/knife.py diff --git a/infinigen/assets/tableware/knife.py b/infinigen/assets/tableware/knife.py new file mode 100644 index 000000000..8f3fb9f81 --- /dev/null +++ b/infinigen/assets/tableware/knife.py @@ -0,0 +1,94 @@ +# Copyright (c) Princeton University. +# This 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 + + +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') From 6d9127d7857ca0337978efc9ca2d0003170b9ef2 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 291/727] Add 1 lines to infinigen/assets/tableware/knife.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tableware/knife.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/knife.py b/infinigen/assets/tableware/knife.py index 8f3fb9f81..39a753c35 100644 --- a/infinigen/assets/tableware/knife.py +++ b/infinigen/assets/tableware/knife.py @@ -12,6 +12,7 @@ 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): From ccb95d9f367a587291a795c94dea8a470fbd35db Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 292/727] Add 44 lines to infinigen/assets/tableware/plate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/plate.py | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 infinigen/assets/tableware/plate.py 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 From 8abaf496424313c2df39beaa60ff3d0b32a09e19 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 293/727] Add 70 lines to infinigen/assets/tableware/jar.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/jar.py | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 infinigen/assets/tableware/jar.py diff --git a/infinigen/assets/tableware/jar.py b/infinigen/assets/tableware/jar.py new file mode 100644 index 000000000..773b3954b --- /dev/null +++ b/infinigen/assets/tableware/jar.py @@ -0,0 +1,70 @@ +# Copyright (c) Princeton University. +# This 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 + +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) + + 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') From ece52cec1bb7661d389db013a85d5debcea00ab1 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 294/727] Add 13 lines to infinigen/assets/tableware/jar.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/jar.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/tableware/jar.py b/infinigen/assets/tableware/jar.py index 773b3954b..79a1e9598 100644 --- a/infinigen/assets/tableware/jar.py +++ b/infinigen/assets/tableware/jar.py @@ -12,6 +12,7 @@ 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): @@ -25,6 +26,14 @@ def __init__(self, factory_seed, coarse=False): 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: @@ -68,3 +77,7 @@ def create_asset(self, **params) -> bpy.types.Object: 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) From 63820a10f8fa058072dee7198b005963747af0c3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 295/727] Add 123 lines to infinigen/assets/tableware/cup.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/cup.py | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 infinigen/assets/tableware/cup.py diff --git a/infinigen/assets/tableware/cup.py b/infinigen/assets/tableware/cup.py new file mode 100644 index 000000000..28bc13ae0 --- /dev/null +++ b/infinigen/assets/tableware/cup.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.tableware.base import TablewareFactory +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 + + +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) + 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') From c0ecbc92bfd2fbaa188d198ab41e1be626a6917e Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 296/727] Add 13 lines to infinigen/assets/tableware/cup.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/cup.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/tableware/cup.py b/infinigen/assets/tableware/cup.py index 28bc13ae0..7a6cc1ea9 100644 --- a/infinigen/assets/tableware/cup.py +++ b/infinigen/assets/tableware/cup.py @@ -7,6 +7,7 @@ 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 @@ -15,6 +16,7 @@ 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): @@ -49,7 +51,14 @@ def __init__(self, factory_seed, coarse=False): 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) @@ -121,3 +130,7 @@ def finalize_assets(self, 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 From 598d41b51893a7271fefc94519bb4a75ef63d7ea Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 297/727] Add 77 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/food_bag.py | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 infinigen/assets/tableware/food_bag.py diff --git a/infinigen/assets/tableware/food_bag.py b/infinigen/assets/tableware/food_bag.py new file mode 100644 index 000000000..ab2a6a5a7 --- /dev/null +++ b/infinigen/assets/tableware/food_bag.py @@ -0,0 +1,77 @@ +# Copyright (c) Princeton University. +# This 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 + + +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) + 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) + 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) From 32ff971bd6d8050ad97bd72096331fd00accb75b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 298/727] Add 5 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/food_bag.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/tableware/food_bag.py b/infinigen/assets/tableware/food_bag.py index ab2a6a5a7..b49c566e2 100644 --- a/infinigen/assets/tableware/food_bag.py +++ b/infinigen/assets/tableware/food_bag.py @@ -16,6 +16,7 @@ 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): @@ -33,6 +34,10 @@ def __init__(self, factory_seed, coarse=False): 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: From 1de1ceb9b811cd18bec115ba0a58cf06057a8f46 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 299/727] Add 1 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/food_bag.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/food_bag.py b/infinigen/assets/tableware/food_bag.py index b49c566e2..68e986d63 100644 --- a/infinigen/assets/tableware/food_bag.py +++ b/infinigen/assets/tableware/food_bag.py @@ -45,6 +45,7 @@ def create_asset(self, **params) -> bpy.types.Object: 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): From 27b38d7fe378602fea582664ae9bb12a9a97fa2e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 300/727] Add 88 lines to infinigen/assets/tableware/fork.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/fork.py | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 infinigen/assets/tableware/fork.py diff --git a/infinigen/assets/tableware/fork.py b/infinigen/assets/tableware/fork.py new file mode 100644 index 000000000..4a9efd424 --- /dev/null +++ b/infinigen/assets/tableware/fork.py @@ -0,0 +1,88 @@ +# Copyright (c) Princeton University. +# This 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 + + +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) From 3b3bc0d1fe599fbdab7c9dbbec26bf26d6c7962f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 301/727] Add 1 lines to infinigen/assets/tableware/fork.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tableware/fork.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/fork.py b/infinigen/assets/tableware/fork.py index 4a9efd424..86c3e949b 100644 --- a/infinigen/assets/tableware/fork.py +++ b/infinigen/assets/tableware/fork.py @@ -12,6 +12,7 @@ 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): From 2d5ffdebf4545ea86cb0698cb1f62466aa878d0a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 302/727] Add 29 lines to infinigen/assets/tableware/food_box.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/food_box.py | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 infinigen/assets/tableware/food_box.py diff --git a/infinigen/assets/tableware/food_box.py b/infinigen/assets/tableware/food_box.py new file mode 100644 index 000000000..29cc951d0 --- /dev/null +++ b/infinigen/assets/tableware/food_box.py @@ -0,0 +1,29 @@ +# Copyright (c) Princeton University. +# This 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 + obj = new_cube() + obj.scale = self.dimensions / 2 + butil.apply_transform(obj) + return obj From a2256d875638f9ef6c875bdee8260322a02c3ad9 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 303/727] Add 8 lines to infinigen/assets/tableware/food_box.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/food_box.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/assets/tableware/food_box.py b/infinigen/assets/tableware/food_box.py index 29cc951d0..e0b645d7a 100644 --- a/infinigen/assets/tableware/food_box.py +++ b/infinigen/assets/tableware/food_box.py @@ -23,7 +23,15 @@ def __init__(self, factory_seed, coarse=False): 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 From f4b6857fa5b595c739234e76ee31237642b80fa0 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 304/727] Add 22 lines to infinigen/assets/tableware/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 infinigen/assets/tableware/__init__.py 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 From 667bbb098e0f85d7616467ab2da86d36b9b2f28e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 305/727] Add 57 lines to infinigen/assets/tableware/spoon.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/spoon.py | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 infinigen/assets/tableware/spoon.py diff --git a/infinigen/assets/tableware/spoon.py b/infinigen/assets/tableware/spoon.py new file mode 100644 index 000000000..0b3abfcd7 --- /dev/null +++ b/infinigen/assets/tableware/spoon.py @@ -0,0 +1,57 @@ +# Copyright (c) Princeton University. +# This 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 + + +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 From f8ffb506e3cf3b721700d119ffca194053c945e6 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 306/727] Add 1 lines to infinigen/assets/tableware/spoon.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tableware/spoon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/spoon.py b/infinigen/assets/tableware/spoon.py index 0b3abfcd7..5570a1733 100644 --- a/infinigen/assets/tableware/spoon.py +++ b/infinigen/assets/tableware/spoon.py @@ -11,6 +11,7 @@ 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): From fe2fef7fc2546c45d5ce98ba18c9e991348c68ec Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 307/727] Add 83 lines to infinigen/assets/tableware/can.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/can.py | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 infinigen/assets/tableware/can.py diff --git a/infinigen/assets/tableware/can.py b/infinigen/assets/tableware/can.py new file mode 100644 index 000000000..b5c15d65f --- /dev/null +++ b/infinigen/assets/tableware/can.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 +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.core.util.math import FixedSeed +from infinigen.core.util.random import log_uniform +from infinigen.core.util import blender as butil + + +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 + 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 From a63619f6b5ee6d3d8facf33dbf560072dd0d0dcd Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 308/727] Add 14 lines to infinigen/assets/tableware/can.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/can.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/infinigen/assets/tableware/can.py b/infinigen/assets/tableware/can.py index b5c15d65f..c41d60528 100644 --- a/infinigen/assets/tableware/can.py +++ b/infinigen/assets/tableware/can.py @@ -15,9 +15,11 @@ 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): @@ -28,6 +30,18 @@ def __init__(self, factory_seed, coarse=False): 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: From 3a5ef39323d1b30dfc879929119cff4962234894 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 309/727] Add 49 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/bowl.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 infinigen/assets/tableware/bowl.py diff --git a/infinigen/assets/tableware/bowl.py b/infinigen/assets/tableware/bowl.py new file mode 100644 index 000000000..8953116bf --- /dev/null +++ b/infinigen/assets/tableware/bowl.py @@ -0,0 +1,49 @@ +# Copyright (c) Princeton University. +# This 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) + + 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) + obj.scale = [self.scale] * 3 + butil.apply_transform(obj) + subsurf(obj, 1) + set_shade_smooth(obj) + return obj From 0b3828005321f824a08a482006f7355cb986744a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 310/727] Add 1 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/bowl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/bowl.py b/infinigen/assets/tableware/bowl.py index 8953116bf..f8f63c096 100644 --- a/infinigen/assets/tableware/bowl.py +++ b/infinigen/assets/tableware/bowl.py @@ -30,6 +30,7 @@ def __init__(self, factory_seed, coarse=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 From 47e18afae6501e2592cf52486db952891651800d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 311/727] Add 1 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/bowl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tableware/bowl.py b/infinigen/assets/tableware/bowl.py index f8f63c096..f0e3e1f94 100644 --- a/infinigen/assets/tableware/bowl.py +++ b/infinigen/assets/tableware/bowl.py @@ -43,6 +43,7 @@ def create_asset(self, **params) -> bpy.types.Object: 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) From aebd70b3ca17b062155512cd80d3cbda3dbde5f9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 312/727] Add 119 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/pan.py | 119 ++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 infinigen/assets/tableware/pan.py diff --git a/infinigen/assets/tableware/pan.py b/infinigen/assets/tableware/pan.py new file mode 100644 index 000000000..9e7a5c2cf --- /dev/null +++ b/infinigen/assets/tableware/pan.py @@ -0,0 +1,119 @@ +# Copyright (c) Princeton University. + +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 ..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 + 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') From b51b72abee84334be26741ed492db91546531d4b Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 313/727] Add 4 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/tableware/pan.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/tableware/pan.py b/infinigen/assets/tableware/pan.py index 9e7a5c2cf..6172c6eab 100644 --- a/infinigen/assets/tableware/pan.py +++ b/infinigen/assets/tableware/pan.py @@ -1,4 +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. + import bpy import bmesh @@ -117,3 +120,4 @@ def add_handle_hole(self, obj): 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) From 1d4f15671fd6512b3af6d998e4f57378b8f2ad5e Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:20 -0700 Subject: [PATCH 314/727] Add 3 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/pan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/tableware/pan.py b/infinigen/assets/tableware/pan.py index 6172c6eab..7921295ac 100644 --- a/infinigen/assets/tableware/pan.py +++ b/infinigen/assets/tableware/pan.py @@ -16,6 +16,7 @@ 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 @@ -41,6 +42,8 @@ def __init__(self, factory_seed, coarse=False): 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 From dc13ce52eafc162589a2b04fc1cf976a435277fa Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 315/727] Add 3 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/pan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/tableware/pan.py b/infinigen/assets/tableware/pan.py index 7921295ac..5e9c53b1a 100644 --- a/infinigen/assets/tableware/pan.py +++ b/infinigen/assets/tableware/pan.py @@ -2,6 +2,9 @@ # This 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 From f963069b48c2fe0cea03e11056f67e43b977f2a2 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 316/727] Add 131 lines to infinigen/assets/tableware/bottle.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/bottle.py | 131 +++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 infinigen/assets/tableware/bottle.py diff --git a/infinigen/assets/tableware/bottle.py b/infinigen/assets/tableware/bottle.py new file mode 100644 index 000000000..8dc20042d --- /dev/null +++ b/infinigen/assets/tableware/bottle.py @@ -0,0 +1,131 @@ +# Copyright (c) Princeton University. +# This 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.core.util.math import FixedSeed +from infinigen.core.util import blender as butil + + +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] + + + 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): + self.scratch.apply(assets) + 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 From 82f28bcbf7919fd3330dd0287e96e48d348d4831 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 317/727] Add 15 lines to infinigen/assets/tableware/bottle.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/bottle.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/assets/tableware/bottle.py b/infinigen/assets/tableware/bottle.py index 8dc20042d..7868fa4d4 100644 --- a/infinigen/assets/tableware/bottle.py +++ b/infinigen/assets/tableware/bottle.py @@ -12,8 +12,10 @@ 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): @@ -75,6 +77,17 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -88,7 +101,9 @@ def create_asset(self, **params) -> bpy.types.Object: 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): From 11c6762271535ddade7678f7693fbc038a3c3ca0 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 318/727] Add 98 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/plant_container.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 infinigen/assets/tableware/plant_container.py diff --git a/infinigen/assets/tableware/plant_container.py b/infinigen/assets/tableware/plant_container.py new file mode 100644 index 000000000..efd38a7ef --- /dev/null +++ b/infinigen/assets/tableware/plant_container.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: 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 + + +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 + self.scale = log_uniform(.08, .12) + + +class PlantContainerFactory(AssetFactory): + 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) + 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: + -self.side_size, + self.side_size, + -self.side_size, + self.side_size, + -.02, + + 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 + selection = np.zeros_like(z).astype(bool) + 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) + butil.apply_modifiers(dirt_) + 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): + + 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 From 350ad95e9753960d34a484aa6a14919850facee6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 319/727] Add 18 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/plant_container.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infinigen/assets/tableware/plant_container.py b/infinigen/assets/tableware/plant_container.py index efd38a7ef..b28ea5fe9 100644 --- a/infinigen/assets/tableware/plant_container.py +++ b/infinigen/assets/tableware/plant_container.py @@ -35,6 +35,7 @@ def __init__(self, factory_seed, coarse=False): class PlantContainerFactory(AssetFactory): + plant_factories = [CactusFactory, MushroomFactory, FernFactory, SucculentFactory, SpiderPlantFactory, SnakePlantFactory] def __init__(self, factory_seed, coarse=False): @@ -48,23 +49,34 @@ def __init__(self, factory_seed, coarse=False): 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) @@ -73,10 +85,14 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object: bpy.ops.mesh.fill_grid() subsurf(dirt_, 3) 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 @@ -84,11 +100,13 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object: ) 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) From 02451aabdba7fe438d392d001ca81428eb441cf5 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 320/727] Add 7 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/tableware/plant_container.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/tableware/plant_container.py b/infinigen/assets/tableware/plant_container.py index b28ea5fe9..6f37fc450 100644 --- a/infinigen/assets/tableware/plant_container.py +++ b/infinigen/assets/tableware/plant_container.py @@ -20,6 +20,7 @@ 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.constraints.example_solver.room.constants import WALL_HEIGHT, WALL_THICKNESS class PlantPotFactory(PotFactory): @@ -114,3 +115,9 @@ def __init__(self, factory_seed, coarse=False): 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=}") From 49f4130ef56b1e8bbececa38493f5637873a3778 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 321/727] Add 6 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/plant_container.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/tableware/plant_container.py b/infinigen/assets/tableware/plant_container.py index 6f37fc450..f2eb58358 100644 --- a/infinigen/assets/tableware/plant_container.py +++ b/infinigen/assets/tableware/plant_container.py @@ -20,6 +20,7 @@ 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 @@ -32,6 +33,8 @@ def __init__(self, factory_seed, coarse=False): 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) @@ -44,6 +47,8 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -85,6 +90,7 @@ def create_asset(self, i, **params) -> bpy.types.Object: 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) From 6a714869c17a1fee7efc5a7ec86a52cbc2df5fad Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 322/727] Add 46 lines to infinigen/assets/tableware/wineglass.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/wineglass.py | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 infinigen/assets/tableware/wineglass.py diff --git a/infinigen/assets/tableware/wineglass.py b/infinigen/assets/tableware/wineglass.py new file mode 100644 index 000000000..f4d01dc9d --- /dev/null +++ b/infinigen/assets/tableware/wineglass.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 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) + return obj From bd67f76d49aef30807d72e6ba3c8d180ee76076d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 323/727] Add 4 lines to infinigen/assets/tableware/wineglass.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tableware/wineglass.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/tableware/wineglass.py b/infinigen/assets/tableware/wineglass.py index f4d01dc9d..463efae45 100644 --- a/infinigen/assets/tableware/wineglass.py +++ b/infinigen/assets/tableware/wineglass.py @@ -43,4 +43,8 @@ def create_asset(self, **params) -> bpy.types.Object: subsurf(obj, 1) obj.scale = [self.scale] * 3 butil.apply_transform(obj) + + with butil.SelectObjects(obj): + bpy.ops.object.shade_smooth() + return obj From a0296acb0944214efc04a6529bdbdb188cd643fb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 324/727] Add 101 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/base.py | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 infinigen/assets/tableware/base.py diff --git a/infinigen/assets/tableware/base.py b/infinigen/assets/tableware/base.py new file mode 100644 index 000000000..6b38185d6 --- /dev/null +++ b/infinigen/assets/tableware/base.py @@ -0,0 +1,101 @@ +# Copyright (c) Princeton University. +# This 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 + + +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, + self.inside_surface = material_assignments['inside'].assign_material() + + + 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) + + 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_']) From 4ae4f1bd1b5fd9b35f7b389bbc4050c842d77618 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 325/727] Add 13 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/tableware/base.py b/infinigen/assets/tableware/base.py index 6b38185d6..e6e41359f 100644 --- a/infinigen/assets/tableware/base.py +++ b/infinigen/assets/tableware/base.py @@ -14,6 +14,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.core.util import blender as butil +from infinigen.assets.material_assignments import AssetList class TablewareFactory(AssetFactory): @@ -25,10 +26,18 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -82,6 +91,10 @@ def finalize_assets(self, assets): 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]) From 40d083831c77c797e14f4eca1e01f50260f4cb8f Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 326/727] Add 2 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tableware/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/tableware/base.py b/infinigen/assets/tableware/base.py index e6e41359f..8940bc8f7 100644 --- a/infinigen/assets/tableware/base.py +++ b/infinigen/assets/tableware/base.py @@ -14,6 +14,8 @@ 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 From 3744bc169fbf1ab1e8212c439b6083202f68e02b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 327/727] Add 100 lines to infinigen/assets/tableware/pot.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/pot.py | 100 ++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 infinigen/assets/tableware/pot.py 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]) From a87513e3ad07d2d59c5dd688fa5be00ec26adc5f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 328/727] Add 111 lines to infinigen/assets/tableware/lid.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tableware/lid.py | 111 ++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 infinigen/assets/tableware/lid.py diff --git a/infinigen/assets/tableware/lid.py b/infinigen/assets/tableware/lid.py new file mode 100644 index 000000000..989dc6d76 --- /dev/null +++ b/infinigen/assets/tableware/lid.py @@ -0,0 +1,111 @@ +# Copyright (c) Princeton University. +# This 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 + + +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) + + + + 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) From 7dfaa5e689911380a6e4905930411a970777b6c8 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 329/727] Add 12 lines to infinigen/assets/tableware/lid.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tableware/lid.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/assets/tableware/lid.py b/infinigen/assets/tableware/lid.py index 989dc6d76..083c2249c 100644 --- a/infinigen/assets/tableware/lid.py +++ b/infinigen/assets/tableware/lid.py @@ -12,6 +12,7 @@ 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): @@ -33,7 +34,18 @@ def __init__(self, factory_seed, coarse=False): 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 From cb080f69fe383500499b92dae0df4577c012b417 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 330/727] Add 514 lines to infinigen/assets/tables/table_utils.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/table_utils.py | 514 +++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 infinigen/assets/tables/table_utils.py 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 From de342fe11ff41b4fbde442f3095a64db8fb66093 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 331/727] Add 167 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/table_top.py | 167 +++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 infinigen/assets/tables/table_top.py diff --git a/infinigen/assets/tables/table_top.py b/infinigen/assets/tables/table_top.py new file mode 100644 index 000000000..9dd4120f5 --- /dev/null +++ b/infinigen/assets/tables/table_top.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: 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 + +@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'}) + + + 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}) + + +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 From 8c28b1f794184fe5e248ff0140b170f159da0b62 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 332/727] Add 4 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tables/table_top.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/tables/table_top.py b/infinigen/assets/tables/table_top.py index 9dd4120f5..da99d0c5e 100644 --- a/infinigen/assets/tables/table_top.py +++ b/infinigen/assets/tables/table_top.py @@ -119,6 +119,10 @@ def nodegroup_generate_table_top(nw: NodeWrangler): 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 From a87e9d485bff5e930ed24f955497e7baa03a2853 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 333/727] Add 3 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tables/table_top.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/tables/table_top.py b/infinigen/assets/tables/table_top.py index da99d0c5e..6aa594807 100644 --- a/infinigen/assets/tables/table_top.py +++ b/infinigen/assets/tables/table_top.py @@ -17,6 +17,8 @@ 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): @@ -110,6 +112,7 @@ def nodegroup_generate_table_top(nw: NodeWrangler): 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]}) From 8546a4eaedbef36fa5d82c202b4d3016fc5da675 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 334/727] Add 298 lines to infinigen/assets/tables/lofting.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/lofting.py | 298 +++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 infinigen/assets/tables/lofting.py 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 From b40d250951f618fafab68002e0882e3e96a89e13 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 335/727] Add 6 lines to infinigen/assets/tables/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tables/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 infinigen/assets/tables/__init__.py diff --git a/infinigen/assets/tables/__init__.py b/infinigen/assets/tables/__init__.py new file mode 100644 index 000000000..688a36ac9 --- /dev/null +++ b/infinigen/assets/tables/__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 .cocktail_table import TableCocktailFactory +from .table_top import TableTopFactory From aacf0af7542cb5d3afa7dabc1d841ca60ec17006 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 336/727] Add 1 lines to infinigen/assets/tables/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tables/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tables/__init__.py b/infinigen/assets/tables/__init__.py index 688a36ac9..96d9cfa0b 100644 --- a/infinigen/assets/tables/__init__.py +++ b/infinigen/assets/tables/__init__.py @@ -3,4 +3,5 @@ # Authors: Lingjie Mei from .cocktail_table import TableCocktailFactory +from .dining_table import TableDiningFactory, SideTableFactory, CoffeeTableFactory from .table_top import TableTopFactory From 3a8f91e6a3d42cc31d5ba381cb6a9900a84a0da5 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 337/727] Add 138 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/dining_table.py | 138 ++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 infinigen/assets/tables/dining_table.py diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py new file mode 100644 index 000000000..40c338eee --- /dev/null +++ b/infinigen/assets/tables/dining_table.py @@ -0,0 +1,138 @@ +# Authors: Yiming Zuo + +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.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_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 + + +@node_utils.to_nodegroup('geometry_create_legs', singleton=False, type='GeometryNodeTree') +def geometry_create_legs(nw: NodeWrangler, **kwargs): + + if kwargs['Leg Style'] == "single_stand": + + '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": + + strecher = nw.new_node(nodegroup_strecher().name, + + '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": + 'Table Height': kwargs['Top Height'], + 'Leg Bottom Relative Scale': kwargs['Leg Placement Bottom Relative Scale'], + 'Align Leg X rot': True + + else: + raise NotImplementedError + + +def geometry_assemble_table(nw: NodeWrangler, **kwargs): + # Code generated using version 2.6.4 of the node_transpiler + + + legs = nw.new_node(geometry_create_legs(**kwargs).name) + + join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [tabletop_instance, legs]}) + + +class TableDiningFactory(AssetFactory): + super(TableDiningFactory, self).__init__(factory_seed, coarse=coarse) + + self.dimensions = dimensions + + with FixedSeed(factory_seed): + self.params = self.sample_parameters(dimensions) + @staticmethod + def sample_parameters(dimensions): + # 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 + + + 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 + + + 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), + '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, + 'Strecher Relative Pos': uniform(0.2, 0.6), + 'Strecher Increament': choice([0, 1, 2]) + } + + return parameters + + def create_asset(self, **params): + 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) + From b18e25977a8c6f065849897e0622c692bcb3c5e8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 338/727] Add 88 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tables/dining_table.py | 90 ++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py index 40c338eee..e2a50cd0f 100644 --- a/infinigen/assets/tables/dining_table.py +++ b/infinigen/assets/tables/dining_table.py @@ -1,11 +1,19 @@ +# Copyright (c) Princeton University. +# This 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.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 @@ -21,44 +29,107 @@ from infinigen.assets.tables.strechers import nodegroup_strecher +from infinigen.core.util.random import log_uniform + @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 + 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']) + }) 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): super(TableDiningFactory, self).__init__(factory_seed, coarse=coarse) @@ -67,6 +138,12 @@ class TableDiningFactory(AssetFactory): 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() @staticmethod def sample_parameters(dimensions): # all in meters @@ -79,7 +156,10 @@ def sample_parameters(dimensions): 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 @@ -89,6 +169,7 @@ def sample_parameters(dimensions): leg_diameter = uniform(0.07, 0.10) leg_curve_ctrl_pts = None + top_scale = 0.8 bottom_scale = 1.0 @@ -130,9 +211,16 @@ def sample_parameters(dimensions): 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) + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) From 57ec967e424b51532992bc7699ecc407130baab3 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 339/727] Add 44 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tables/dining_table.py | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py index e2a50cd0f..dcb660594 100644 --- a/infinigen/assets/tables/dining_table.py +++ b/infinigen/assets/tables/dining_table.py @@ -132,6 +132,7 @@ def geometry_assemble_table(nw: NodeWrangler, **kwargs): class TableDiningFactory(AssetFactory): + def __init__(self, factory_seed, coarse=False, dimensions=None): super(TableDiningFactory, self).__init__(factory_seed, coarse=coarse) self.dimensions = dimensions @@ -146,6 +147,24 @@ class TableDiningFactory(AssetFactory): self.clothes_scatter = NoApply() @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 @@ -186,6 +205,7 @@ def sample_parameters(dimensions): else: raise NotImplementedError + top_thickness = uniform(0.03, 0.06) parameters = { 'Top Profile N-gon': NGon, @@ -218,9 +238,33 @@ def create_asset(self, **params): # 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): self.scratch.apply(assets) 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) + From 898c600ebc040b4f78e901a319da2ed76147a20a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 340/727] Add 41 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tables/dining_table.py | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py index dcb660594..250b65809 100644 --- a/infinigen/assets/tables/dining_table.py +++ b/infinigen/assets/tables/dining_table.py @@ -11,6 +11,8 @@ 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 @@ -30,6 +32,7 @@ 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') @@ -101,6 +104,8 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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}) @@ -122,6 +127,9 @@ def geometry_assemble_table(nw: NodeWrangler, **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) @@ -145,6 +153,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): # 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): @@ -214,6 +250,7 @@ def sample_parameters(dimensions): '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, @@ -224,6 +261,7 @@ def sample_parameters(dimensions): '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]) } @@ -243,8 +281,11 @@ def create_asset(self, **params): 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) From cd301e1415b892ca8c48c1a7af442253cacd45d3 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 341/727] Add 1 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tables/dining_table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tables/dining_table.py b/infinigen/assets/tables/dining_table.py index 250b65809..9c956b499 100644 --- a/infinigen/assets/tables/dining_table.py +++ b/infinigen/assets/tables/dining_table.py @@ -18,6 +18,7 @@ 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 ed079b4ac31dab67e0d3bd0ca7f60f9d0f082377 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:21 -0700 Subject: [PATCH 342/727] Add 166 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/cocktail_table.py | 166 ++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 infinigen/assets/tables/cocktail_table.py diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py new file mode 100644 index 000000000..d42cd6964 --- /dev/null +++ b/infinigen/assets/tables/cocktail_table.py @@ -0,0 +1,166 @@ + +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.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_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 + + +@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, + 'Resolution': 64}) + + leg = nw.new_node(nodegroup_create_legs_and_strechers().name, + '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, + '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={ + '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={ + '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, + 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, + 'Fillet Ratio': kwargs['Top Profile Fillet Ratio'], + 'Translation': (0.0000, 0.0000, kwargs['Top Height'])}) + + tabletop_instance = nw.new_node(Nodes.SetMaterial, + + legs = nw.new_node(geometry_create_legs(**kwargs).name) + + join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [tabletop_instance, legs]}) + + +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) + @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) + + (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), + '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, + 'Strecher Relative Pos': uniform(0.2, 0.6), + 'Strecher Increament': choice([0, 1, 2]) + } + + return parameters + + + 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 + + tagging.tag_system.relabel_obj(obj) + + return obj + From 2a1ee2a45d0f8be218316a636dd559eb10915967 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 343/727] Add 37 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/tables/cocktail_table.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py index d42cd6964..f9343192e 100644 --- a/infinigen/assets/tables/cocktail_table.py +++ b/infinigen/assets/tables/cocktail_table.py @@ -1,10 +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. + 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 @@ -20,6 +25,7 @@ from infinigen.assets.tables.strechers import nodegroup_strecher +from infinigen.core.util.random import log_uniform @node_utils.to_nodegroup('geometry_create_legs', singleton=False, type='GeometryNodeTree') def geometry_create_legs(nw: NodeWrangler, **kwargs): @@ -29,9 +35,14 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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 @@ -39,6 +50,10 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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, @@ -46,6 +61,9 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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'], @@ -57,6 +75,8 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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'], @@ -68,13 +88,20 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): raise NotImplementedError leg = nw.new_node(Nodes.SetMaterial, + 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, + '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'], + + 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, @@ -84,6 +111,7 @@ def geometry_assemble_table(nw: NodeWrangler, **kwargs): join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [tabletop_instance, legs]}) + class TableCocktailFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=None): super(TableCocktailFactory, self).__init__(factory_seed, coarse=coarse) @@ -92,6 +120,12 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): 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() + @staticmethod def sample_parameters(dimensions): # all in meters @@ -115,6 +149,7 @@ def sample_parameters(dimensions): 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": @@ -164,3 +199,5 @@ def sample_parameters(dimensions): return obj + def finalize_assets(self, assets): + self.clothes_scatter.apply(assets) From a11ca82983345dd0232cda0b608037e1f4f98bdb Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 344/727] Add 35 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/tables/cocktail_table.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py index f9343192e..d1988688a 100644 --- a/infinigen/assets/tables/cocktail_table.py +++ b/infinigen/assets/tables/cocktail_table.py @@ -26,6 +26,7 @@ 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): @@ -88,6 +89,7 @@ def geometry_create_legs(nw: NodeWrangler, **kwargs): 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}) @@ -105,6 +107,7 @@ def geometry_assemble_table(nw: NodeWrangler, **kwargs): '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) @@ -125,6 +128,32 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): # 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): @@ -174,6 +203,7 @@ def sample_parameters(dimensions): '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, @@ -184,6 +214,7 @@ def sample_parameters(dimensions): '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]) } @@ -201,3 +232,7 @@ def sample_parameters(dimensions): 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) From 76360dd1ec5ff3d32715bf6e6f87b1461160cfa9 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 345/727] Add 30 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/tables/cocktail_table.py | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py index d1988688a..67c36f1be 100644 --- a/infinigen/assets/tables/cocktail_table.py +++ b/infinigen/assets/tables/cocktail_table.py @@ -1,6 +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: +# - Yiming Zuo: primary author +# - Alexander Raistrick: implement placeholder import bpy import bpy @@ -97,10 +100,14 @@ 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, @@ -113,6 +120,20 @@ def geometry_assemble_table(nw: NodeWrangler, **kwargs): 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): @@ -221,14 +242,23 @@ def sample_parameters(dimensions): 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) From 1c8c0ec20efd395abf339b55a12bf2ece48035b9 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 346/727] Add 1 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/tables/cocktail_table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/tables/cocktail_table.py b/infinigen/assets/tables/cocktail_table.py index 67c36f1be..ec7e30f7d 100644 --- a/infinigen/assets/tables/cocktail_table.py +++ b/infinigen/assets/tables/cocktail_table.py @@ -15,6 +15,7 @@ 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 2efb90389380197647bda10e477681e200befdcd Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 347/727] Add 33 lines to infinigen/assets/tables/strechers.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/strechers.py | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 infinigen/assets/tables/strechers.py 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 From 64a08a7756f667c44a394d852e388f3120e08de8 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 348/727] Add 34 lines to infinigen/assets/tables/legs/single_stand.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/legs/single_stand.py | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 infinigen/assets/tables/legs/single_stand.py 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 From 48535c4d8101e6eb6f42cc6ac2bc6c75e8450680 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 349/727] Add 217 lines to infinigen/assets/tables/legs/wheeled.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/legs/wheeled.py | 217 ++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 infinigen/assets/tables/legs/wheeled.py 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 From fcf2965155bd9f426c013acd603614718e2f0488 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 350/727] Add 74 lines to infinigen/assets/tables/legs/square.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/legs/square.py | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infinigen/assets/tables/legs/square.py 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 From 98d66875e0ac91a1e155967995ccca8cfa0e4073 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 351/727] Add 36 lines to infinigen/assets/tables/legs/straight.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/tables/legs/straight.py | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 infinigen/assets/tables/legs/straight.py 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 From aefd2957994b2a13008ab6b44b25bd26063c79f9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 352/727] Add 341 lines to infinigen/assets/materials/tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/tile.py | 341 +++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 infinigen/assets/materials/tile.py diff --git a/infinigen/assets/materials/tile.py b/infinigen/assets/materials/tile.py new file mode 100644 index 000000000..325a34b2f --- /dev/null +++ b/infinigen/assets/materials/tile.py @@ -0,0 +1,341 @@ +# Copyright (c) Princeton University. +# This 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 + + +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()) + if shader_func is None: + shader_func = np.random.choice(funcs, p=weights) + 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 From 3d4b67efc69ff065ea41fe71128576a4880709b4 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 353/727] Add 5 lines to infinigen/assets/materials/tile.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/tile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/tile.py b/infinigen/assets/materials/tile.py index 325a34b2f..a926f67d9 100644 --- a/infinigen/assets/materials/tile.py +++ b/infinigen/assets/materials/tile.py @@ -15,6 +15,8 @@ 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 @@ -304,8 +306,11 @@ def get_shader_funcs(): 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.) From 87539df7692a4956877325fb053c2f9d48f5731e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 354/727] Add 18 lines to infinigen/assets/materials/mirror.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/mirror.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 infinigen/assets/materials/mirror.py 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) From 2b49d630e0085e51c41ee52331519b8486607d61 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 355/727] Add 44 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/hardwood_floor.py | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 infinigen/assets/materials/hardwood_floor.py diff --git a/infinigen/assets/materials/hardwood_floor.py b/infinigen/assets/materials/hardwood_floor.py new file mode 100644 index 000000000..11cad7bb3 --- /dev/null +++ b/infinigen/assets/materials/hardwood_floor.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 numpy as np +from numpy.random import uniform + +from . import common +from .utils.surface_utils import perturb_coordinates +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 From bcd2e86bc96e5cbdfdb5738a1bb921577fd0d4cd Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 356/727] Add 1 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/materials/hardwood_floor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/hardwood_floor.py b/infinigen/assets/materials/hardwood_floor.py index 11cad7bb3..4a29ca9cb 100644 --- a/infinigen/assets/materials/hardwood_floor.py +++ b/infinigen/assets/materials/hardwood_floor.py @@ -7,6 +7,7 @@ from . import common from .utils.surface_utils import perturb_coordinates +from infinigen.assets.utils.object import new_plane from ...core.nodes import NodeWrangler, Nodes from ...core.util.random import log_uniform From 30030327e7234a203a79d4de5cfac0bd780e489e Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 357/727] Add 1 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/hardwood_floor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/hardwood_floor.py b/infinigen/assets/materials/hardwood_floor.py index 4a29ca9cb..34ca16bb5 100644 --- a/infinigen/assets/materials/hardwood_floor.py +++ b/infinigen/assets/materials/hardwood_floor.py @@ -7,6 +7,7 @@ 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 From ee938ae9d2f5ea9d47247343f7a81fb7aadff56e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 358/727] Add 176 lines to infinigen/assets/materials/bumpy_rubber_floor.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/materials/bumpy_rubber_floor.py | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 infinigen/assets/materials/bumpy_rubber_floor.py 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 From 45c9f44e689fc017321a7c5ebc6fdb4ce3da182f Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 359/727] Add 80 lines to infinigen/assets/materials/dishwasher_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/materials/dishwasher_shaders.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 infinigen/assets/materials/dishwasher_shaders.py diff --git a/infinigen/assets/materials/dishwasher_shaders.py b/infinigen/assets/materials/dishwasher_shaders.py new file mode 100644 index 000000000..296eeea62 --- /dev/null +++ b/infinigen/assets/materials/dishwasher_shaders.py @@ -0,0 +1,80 @@ +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}) From a96bf8c2a85e854b62d24631b45074d867ea1f38 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 360/727] Add 5 lines to infinigen/assets/materials/dishwasher_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/dishwasher_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/dishwasher_shaders.py b/infinigen/assets/materials/dishwasher_shaders.py index 296eeea62..8f2c5f7f8 100644 --- a/infinigen/assets/materials/dishwasher_shaders.py +++ b/infinigen/assets/materials/dishwasher_shaders.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: Hongyu Wen + from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler def default_shader(nw: NodeWrangler): From 16091f22baed3e24bcc7678680a7d0c2d4620114 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 361/727] Add 27 lines to infinigen/assets/materials/glass_volume.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/glass_volume.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 infinigen/assets/materials/glass_volume.py diff --git a/infinigen/assets/materials/glass_volume.py b/infinigen/assets/materials/glass_volume.py new file mode 100644 index 000000000..8d7fa73af --- /dev/null +++ b/infinigen/assets/materials/glass_volume.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: 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)) + + + volume_absorption = nw.new_node('ShaderNodeVolumeAbsorption', + + material_output = nw.new_node(Nodes.MaterialOutput, + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_glass_volume, selection, **kwargs) From 3b0a79aa7843d171d43a7fb447e048cc1d81723c Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 362/727] Add 4 lines to infinigen/assets/materials/glass_volume.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/materials/glass_volume.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/glass_volume.py b/infinigen/assets/materials/glass_volume.py index 8d7fa73af..84fdc059b 100644 --- a/infinigen/assets/materials/glass_volume.py +++ b/infinigen/assets/materials/glass_volume.py @@ -18,10 +18,14 @@ def shader_glass_volume(nw: NodeWrangler, color=None, density=100.0, **kwargs): 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) From a74a280adc732cc7652612ff1b415b01b6d53817 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 363/727] Add 41 lines to infinigen/assets/materials/beverage_fridge_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../materials/beverage_fridge_shaders.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 infinigen/assets/materials/beverage_fridge_shaders.py diff --git a/infinigen/assets/materials/beverage_fridge_shaders.py b/infinigen/assets/materials/beverage_fridge_shaders.py new file mode 100644 index 000000000..6162a6a42 --- /dev/null +++ b/infinigen/assets/materials/beverage_fridge_shaders.py @@ -0,0 +1,41 @@ +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}) From 500cd32f2803cc2ff7d82947d5e9e719f0304c27 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 364/727] Add 5 lines to infinigen/assets/materials/beverage_fridge_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/beverage_fridge_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/beverage_fridge_shaders.py b/infinigen/assets/materials/beverage_fridge_shaders.py index 6162a6a42..2499733e9 100644 --- a/infinigen/assets/materials/beverage_fridge_shaders.py +++ b/infinigen/assets/materials/beverage_fridge_shaders.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: Hongyu Wen + from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler def shader_glass_001(nw: NodeWrangler): From c08fa552a7c3331c5544efc2550f3f7eda947924 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 365/727] Add 58 lines to infinigen/assets/materials/marble_regular.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/assets/materials/marble_regular.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 infinigen/assets/materials/marble_regular.py 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 From bd9ac03982c0ce20c40331d3a43366d000f57838 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 366/727] Add 54 lines to infinigen/assets/materials/lamp_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/lamp_shaders.py | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 infinigen/assets/materials/lamp_shaders.py diff --git a/infinigen/assets/materials/lamp_shaders.py b/infinigen/assets/materials/lamp_shaders.py new file mode 100644 index 000000000..2759a13d4 --- /dev/null +++ b/infinigen/assets/materials/lamp_shaders.py @@ -0,0 +1,54 @@ +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}) From cc20e05813c145c914256fb4c2f068177a7fbc64 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 367/727] Add 5 lines to infinigen/assets/materials/lamp_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/lamp_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/lamp_shaders.py b/infinigen/assets/materials/lamp_shaders.py index 2759a13d4..55f437b78 100644 --- a/infinigen/assets/materials/lamp_shaders.py +++ b/infinigen/assets/materials/lamp_shaders.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: Hongyu Wen + from numpy.random import uniform as U, normal as N, randint as RI from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler From 5daeaa48d7c245b8807d18d736f67e7ba4ac359b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 368/727] Add 23 lines to infinigen/assets/materials/oven_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/oven_shaders.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 infinigen/assets/materials/oven_shaders.py diff --git a/infinigen/assets/materials/oven_shaders.py b/infinigen/assets/materials/oven_shaders.py new file mode 100644 index 000000000..db917d5f3 --- /dev/null +++ b/infinigen/assets/materials/oven_shaders.py @@ -0,0 +1,23 @@ +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}) + From 70a7347e0116f9b9e42bd52c5dcf1d8436f5376a Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:22 -0700 Subject: [PATCH 369/727] Add 5 lines to infinigen/assets/materials/oven_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/oven_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/oven_shaders.py b/infinigen/assets/materials/oven_shaders.py index db917d5f3..c6e3017e9 100644 --- a/infinigen/assets/materials/oven_shaders.py +++ b/infinigen/assets/materials/oven_shaders.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: Hongyu Wen + from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler From b14caa6b74641dbbb9f18545fa0d40e0cdbd07ae Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 370/727] Add 22 lines to infinigen/assets/materials/plastic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/plastic.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 infinigen/assets/materials/plastic.py diff --git a/infinigen/assets/materials/plastic.py b/infinigen/assets/materials/plastic.py new file mode 100644 index 000000000..4ac6edf9a --- /dev/null +++ b/infinigen/assets/materials/plastic.py @@ -0,0 +1,22 @@ +# 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) From 00fd65645cc16b4dc6ee19ceedbb5b958c1fa8d7 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 371/727] Add 2 lines to infinigen/assets/materials/plastic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/plastic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/materials/plastic.py b/infinigen/assets/materials/plastic.py index 4ac6edf9a..1dd6912b8 100644 --- a/infinigen/assets/materials/plastic.py +++ b/infinigen/assets/materials/plastic.py @@ -1,7 +1,9 @@ # 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 c83f62f89774693073635bf651ee7a01532f909b Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 372/727] Add 275 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/materials/shelf_shaders.py | 275 ++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 infinigen/assets/materials/shelf_shaders.py diff --git a/infinigen/assets/materials/shelf_shaders.py b/infinigen/assets/materials/shelf_shaders.py new file mode 100644 index 000000000..f0314615d --- /dev/null +++ b/infinigen/assets/materials/shelf_shaders.py @@ -0,0 +1,275 @@ + +# Authors: Beining Han +# Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=jDEijCwz6to by Lachlan Sarv + +import numpy as np + + metal_shader_list, + shader_glass, + + + + + + +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(metal_shader_list) + return surface.shaderfunc_to_material(shader_func, **kwargs) From bc559106b5886fc23bb48a68b1784fb3350338a2 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 373/727] Add 16 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/shelf_shaders.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/infinigen/assets/materials/shelf_shaders.py b/infinigen/assets/materials/shelf_shaders.py index f0314615d..3eab40532 100644 --- a/infinigen/assets/materials/shelf_shaders.py +++ b/infinigen/assets/materials/shelf_shaders.py @@ -1,15 +1,30 @@ +# Copyright (c) Princeton University. +# This 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): @@ -268,6 +283,7 @@ def get_shelf_material(name, **kwargs): 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 _: From f28f8a8c9d9ece8f0c34ec3872419ed7fea02207 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 374/727] Add 7 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/shelf_shaders.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/materials/shelf_shaders.py b/infinigen/assets/materials/shelf_shaders.py index 3eab40532..5801c175a 100644 --- a/infinigen/assets/materials/shelf_shaders.py +++ b/infinigen/assets/materials/shelf_shaders.py @@ -255,6 +255,7 @@ def shader_shelves_wood(nw: NodeWrangler, **kwargs): 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, @@ -287,5 +288,11 @@ def get_shelf_material(name, **kwargs): 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]) shader_func = np.random.choice(metal_shader_list) + 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]) return surface.shaderfunc_to_material(shader_func, **kwargs) From 805f54a567866c00c35482b3222fe5f7cf119159 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 375/727] Add 5 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/shelf_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/shelf_shaders.py b/infinigen/assets/materials/shelf_shaders.py index 5801c175a..ed92d64ad 100644 --- a/infinigen/assets/materials/shelf_shaders.py +++ b/infinigen/assets/materials/shelf_shaders.py @@ -291,8 +291,13 @@ def get_shelf_material(name, **kwargs): 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) From f4c3c4bea58f6a3a69373fa979d77e5d2bb352bf Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 376/727] Add 36 lines to infinigen/assets/materials/vase_shaders.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/materials/vase_shaders.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 infinigen/assets/materials/vase_shaders.py diff --git a/infinigen/assets/materials/vase_shaders.py b/infinigen/assets/materials/vase_shaders.py new file mode 100644 index 000000000..5a7a0d472 --- /dev/null +++ b/infinigen/assets/materials/vase_shaders.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, 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) + + + material_output = nw.new_node(Nodes.MaterialOutput, input_kwargs={'Surface': glass_bsdf}, attrs={'is_active_output': True}) + From 9c81f0397cfb5fef8dfca062b81ca5ed4d6a0852 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 377/727] Add 1 lines to infinigen/assets/materials/vase_shaders.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/materials/vase_shaders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/vase_shaders.py b/infinigen/assets/materials/vase_shaders.py index 5a7a0d472..674f9a553 100644 --- a/infinigen/assets/materials/vase_shaders.py +++ b/infinigen/assets/materials/vase_shaders.py @@ -31,6 +31,7 @@ def shader_glass(nw: NodeWrangler): 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}) From 9497ba6c0e85e5f2c82f3e936ac2cd5c48d20e0c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 378/727] Add 65 lines to infinigen/assets/materials/common.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/common.py | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 infinigen/assets/materials/common.py diff --git a/infinigen/assets/materials/common.py b/infinigen/assets/materials/common.py new file mode 100644 index 000000000..a07960a22 --- /dev/null +++ b/infinigen/assets/materials/common.py @@ -0,0 +1,65 @@ +# Copyright (c) Princeton University. +# This 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.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): + 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): + 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 From 89a25825a4cae1ee3731bd0fbf3f5abba9b54453 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 379/727] Add 3 lines to infinigen/assets/materials/common.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/materials/common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/materials/common.py b/infinigen/assets/materials/common.py index a07960a22..b906c4bcf 100644 --- a/infinigen/assets/materials/common.py +++ b/infinigen/assets/materials/common.py @@ -11,6 +11,7 @@ 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 @@ -29,6 +30,7 @@ def apply(obj, shader_func, selection=None, *args, **kwargs): 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: @@ -45,6 +47,7 @@ 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: From bf9745b6f6cc68b5c919835680cfdb0732414aff Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 380/727] Add 43 lines to infinigen/assets/materials/ceramic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/ceramic.py | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 infinigen/assets/materials/ceramic.py diff --git a/infinigen/assets/materials/ceramic.py b/infinigen/assets/materials/ceramic.py new file mode 100644 index 000000000..dcfd06874 --- /dev/null +++ b/infinigen/assets/materials/ceramic.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: 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) From f13792a92dda18d3ad3b695f04d1fd78f0256b98 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 381/727] Add 1 lines to infinigen/assets/materials/ceramic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/ceramic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/ceramic.py b/infinigen/assets/materials/ceramic.py index dcfd06874..40e63c96f 100644 --- a/infinigen/assets/materials/ceramic.py +++ b/infinigen/assets/materials/ceramic.py @@ -20,6 +20,7 @@ def shader_ceramic(nw: NodeWrangler, clear=False, roughness_min=0, roughness_max 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, From 352e6ebc66af00bea2ae56a57e23d4032473e2c3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 382/727] Add 9 lines to infinigen/assets/materials/marble.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/marble.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 infinigen/assets/materials/marble.py diff --git a/infinigen/assets/materials/marble.py b/infinigen/assets/materials/marble.py new file mode 100644 index 000000000..3d2a82743 --- /dev/null +++ b/infinigen/assets/materials/marble.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 infinigen.assets.materials import common + + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_marble, selection, **kwargs) From ddf98c409475b36b858088582ffcbc4d4d68699a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 383/727] Add 1 lines to infinigen/assets/materials/marble.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/marble.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/marble.py b/infinigen/assets/materials/marble.py index 3d2a82743..51c23644a 100644 --- a/infinigen/assets/materials/marble.py +++ b/infinigen/assets/materials/marble.py @@ -3,6 +3,7 @@ # Authors: Lingjie Mei from infinigen.assets.materials import common +from infinigen.assets.materials.table_materials import shader_marble def apply(obj, selection=None, **kwargs): From 4c4490a142479e0c6cdb5dc99d54528f22019efd Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 384/727] Add 247 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/text.py | 247 +++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 infinigen/assets/materials/text.py diff --git a/infinigen/assets/materials/text.py b/infinigen/assets/materials/text.py new file mode 100644 index 000000000..a40008865 --- /dev/null +++ b/infinigen/assets/materials/text.py @@ -0,0 +1,247 @@ +# Copyright (c) Princeton University. +# 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 colorsys +import inspect +import io + +import matplotlib.font_manager +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + +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.core.nodes.node_info import Nodes +from infinigen.core.nodes.node_wrangler import NodeWrangler +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 + + +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)] + ) + 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)]) + 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 + 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): + roughness = nw.new_node(Nodes.NoiseTexture) + if self.emission > 0: + emission = color + 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) + + + 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 From 16b05dc125c77e02cd7cf0505999b88e8b564a86 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 385/727] Add 35 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/materials/text.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/assets/materials/text.py b/infinigen/assets/materials/text.py index a40008865..d2cc4dc18 100644 --- a/infinigen/assets/materials/text.py +++ b/infinigen/assets/materials/text.py @@ -1,6 +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: 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 @@ -9,6 +14,7 @@ import matplotlib.pyplot as plt 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 @@ -213,6 +219,35 @@ def make_shader_func(self, bbox): 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 From 2e2d49754e07807bf1ebc2c6d17ae2424cdca006 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 386/727] Add 27 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/text.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/infinigen/assets/materials/text.py b/infinigen/assets/materials/text.py index d2cc4dc18..fe7ee649a 100644 --- a/infinigen/assets/materials/text.py +++ b/infinigen/assets/materials/text.py @@ -9,9 +9,15 @@ 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 @@ -20,13 +26,17 @@ 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() @@ -68,6 +78,12 @@ def build_diverging_colormap(): 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 @@ -76,6 +92,11 @@ def build_sequential_colormap(): 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 @@ -186,6 +207,9 @@ def add_patches(self, locs, bbox): ) 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): @@ -251,6 +275,8 @@ def shader_text(nw: NodeWrangler, **kwargs): 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( @@ -271,6 +297,7 @@ 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) From 3918b52ba6f7cce0818ac23040cd11b1dcd2fcd4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 387/727] Add 42 lines to infinigen/assets/materials/rug.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/rug.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 infinigen/assets/materials/rug.py diff --git a/infinigen/assets/materials/rug.py b/infinigen/assets/materials/rug.py new file mode 100644 index 000000000..1fd81599f --- /dev/null +++ b/infinigen/assets/materials/rug.py @@ -0,0 +1,42 @@ +# Copyright (c) Princeton University. +# This 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]) + 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, + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_rug, selection, **kwargs) From 9be42a9c74c91703c618d660efd796cffa342eea Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 388/727] Add 8 lines to infinigen/assets/materials/rug.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/materials/rug.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/assets/materials/rug.py b/infinigen/assets/materials/rug.py index 1fd81599f..4f03d19f2 100644 --- a/infinigen/assets/materials/rug.py +++ b/infinigen/assets/materials/rug.py @@ -20,6 +20,12 @@ def shader_rug(nw: NodeWrangler, strength=1., **kwargs): 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: @@ -37,6 +43,8 @@ def shader_rug(nw: NodeWrangler, strength=1., **kwargs): 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) From 8f6a42d790502b82cf1e91241e27b47c68ab25cc Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 389/727] Add 39 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/materials/ceiling_light_shaders.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 infinigen/assets/materials/ceiling_light_shaders.py diff --git a/infinigen/assets/materials/ceiling_light_shaders.py b/infinigen/assets/materials/ceiling_light_shaders.py new file mode 100644 index 000000000..1738617ab --- /dev/null +++ b/infinigen/assets/materials/ceiling_light_shaders.py @@ -0,0 +1,39 @@ +from numpy.random import uniform as U +from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler +from infinigen.core.util.color import hsv2rgba + + + # 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 From 03034f6483386f1388d7c1c4c66fe329a5025477 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 390/727] Add 5 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/ceiling_light_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/ceiling_light_shaders.py b/infinigen/assets/materials/ceiling_light_shaders.py index 1738617ab..ceb3724e1 100644 --- a/infinigen/assets/materials/ceiling_light_shaders.py +++ b/infinigen/assets/materials/ceiling_light_shaders.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: 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 From c2f30a6c539b40655880a82df0ce5d5ca83b170d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 391/727] Add 1 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/ceiling_light_shaders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/ceiling_light_shaders.py b/infinigen/assets/materials/ceiling_light_shaders.py index ceb3724e1..d738d8230 100644 --- a/infinigen/assets/materials/ceiling_light_shaders.py +++ b/infinigen/assets/materials/ceiling_light_shaders.py @@ -8,6 +8,7 @@ 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) From bb400732beec43b81f185f4ed773010157949e9a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 392/727] Add 99 lines to infinigen/assets/materials/art.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/art.py | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 infinigen/assets/materials/art.py diff --git a/infinigen/assets/materials/art.py b/infinigen/assets/materials/art.py new file mode 100644 index 000000000..963605f01 --- /dev/null +++ b/infinigen/assets/materials/art.py @@ -0,0 +1,99 @@ +# Copyright (c) Princeton University. +# This 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) + + +def make_sphere(): + return text.make_sphere() From 4fef88c9c1afc7994ef2216d82373eba8bad5f6c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 393/727] Add 1 lines to infinigen/assets/materials/art.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/art.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/art.py b/infinigen/assets/materials/art.py index 963605f01..a872158bd 100644 --- a/infinigen/assets/materials/art.py +++ b/infinigen/assets/materials/art.py @@ -93,6 +93,7 @@ def base_shader(self): 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(): From fc339631b2dd6c0b08295e0555713dea59f3e3eb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 394/727] Add 12 lines to infinigen/assets/materials/text_no_barcode.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/text_no_barcode.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infinigen/assets/materials/text_no_barcode.py 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) From 944e99b1b791f774f33cae86239af365647f28b1 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 395/727] Add 27 lines to infinigen/assets/materials/glass.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/glass.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 infinigen/assets/materials/glass.py diff --git a/infinigen/assets/materials/glass.py b/infinigen/assets/materials/glass.py new file mode 100644 index 000000000..df7e14e08 --- /dev/null +++ b/infinigen/assets/materials/glass.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: Lingjie Mei +import colorsys + +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 + + + if color is None: + + +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 From e8c7b751367ba6d0d3eeb260719cd564f3e0ae6d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 396/727] Add 21 lines to infinigen/assets/materials/glass.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/glass.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/infinigen/assets/materials/glass.py b/infinigen/assets/materials/glass.py index df7e14e08..be4c28cbb 100644 --- a/infinigen/assets/materials/glass.py +++ b/infinigen/assets/materials/glass.py @@ -6,14 +6,35 @@ 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) From a2a66fa32d83c10006d3de1e9237a4ee205d0b22 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:23 -0700 Subject: [PATCH 397/727] Add 22 lines to infinigen/assets/materials/microwave_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/materials/microwave_shaders.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 infinigen/assets/materials/microwave_shaders.py diff --git a/infinigen/assets/materials/microwave_shaders.py b/infinigen/assets/materials/microwave_shaders.py new file mode 100644 index 000000000..c29016c69 --- /dev/null +++ b/infinigen/assets/materials/microwave_shaders.py @@ -0,0 +1,22 @@ +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 From cc2ec0f0f02933964cb4ad89713dfc216d2a9873 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 398/727] Add 5 lines to infinigen/assets/materials/microwave_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/microwave_shaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/microwave_shaders.py b/infinigen/assets/materials/microwave_shaders.py index c29016c69..f08b4ff42 100644 --- a/infinigen/assets/materials/microwave_shaders.py +++ b/infinigen/assets/materials/microwave_shaders.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: Hongyu Wen + from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler def shader_black_medal(nw: NodeWrangler): From 8ae82e1c8d80aed6af967d987e963e1896dc474a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 399/727] Add 50 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/plaster.py | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 infinigen/assets/materials/plaster.py diff --git a/infinigen/assets/materials/plaster.py b/infinigen/assets/materials/plaster.py new file mode 100644 index 000000000..4414c1634 --- /dev/null +++ b/infinigen/assets/materials/plaster.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 +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 + + +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)}) + difference = nw.new_node(Nodes.MixRGB, [musgrave, noise], attrs={'blend_type': 'DIFFERENCE'}) + '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), + }) + + +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) + + From 53654732b816671671da7b1c453927f3e88f0851 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 400/727] Add 6 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/materials/plaster.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/materials/plaster.py b/infinigen/assets/materials/plaster.py index 4414c1634..5bb0b0740 100644 --- a/infinigen/assets/materials/plaster.py +++ b/infinigen/assets/materials/plaster.py @@ -32,12 +32,18 @@ def shader_plaster(nw: NodeWrangler, plaster_colored, **kwargs): noise = nw.new_node(Nodes.NoiseTexture, [uv_map], input_kwargs={'Detail': log_uniform(15, 30), 'Distortion': log_uniform(4, 8)}) difference = nw.new_node(Nodes.MixRGB, [musgrave, noise], attrs={'blend_type': 'DIFFERENCE'}) + + 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): From 46969c27192334bf36d7ea317435f59b6bd45647 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 401/727] Add 3 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/plaster.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/materials/plaster.py b/infinigen/assets/materials/plaster.py index 5bb0b0740..918108828 100644 --- a/infinigen/assets/materials/plaster.py +++ b/infinigen/assets/materials/plaster.py @@ -14,6 +14,7 @@ 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): @@ -31,7 +32,9 @@ def shader_plaster(nw: NodeWrangler, plaster_colored, **kwargs): 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), From 93f3c898b70e79094ebd5c6ac48a53d505010b7e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 402/727] Add 105 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/materials/table_materials.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 infinigen/assets/materials/table_materials.py diff --git a/infinigen/assets/materials/table_materials.py b/infinigen/assets/materials/table_materials.py new file mode 100644 index 000000000..190bc93a2 --- /dev/null +++ b/infinigen/assets/materials/table_materials.py @@ -0,0 +1,105 @@ +# Copyright (c) Princeton University. +# This 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 import surface + + # 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, + voronoi_texture = nw.new_node(Nodes.VoronoiTexture, + 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, + attrs={'noise_dimensions': '4D'}) + mix_1 = nw.new_node(Nodes.Mix, + 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, + 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_1 = nw.new_node(Nodes.RGB) + 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}) From 1cf4c416d25629545614112ffad57c69c7f78e7c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 403/727] Add 53 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/table_materials.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/infinigen/assets/materials/table_materials.py b/infinigen/assets/materials/table_materials.py index 190bc93a2..969dfeb62 100644 --- a/infinigen/assets/materials/table_materials.py +++ b/infinigen/assets/materials/table_materials.py @@ -7,99 +7,152 @@ 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, + voronoi_texture = nw.new_node(Nodes.VoronoiTexture, 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, attrs={'noise_dimensions': '4D'}) + mix_1 = nw.new_node(Nodes.Mix, 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, + 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}) From e42bbedd2b666de9b043fc89224ee1bad2faa540 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 404/727] Add 5 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/table_materials.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/table_materials.py b/infinigen/assets/materials/table_materials.py index 969dfeb62..96c8b5756 100644 --- a/infinigen/assets/materials/table_materials.py +++ b/infinigen/assets/materials/table_materials.py @@ -46,8 +46,10 @@ def shader_marble(nw: NodeWrangler,**kwargs): 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"]}) @@ -61,9 +63,11 @@ def shader_marble(nw: NodeWrangler,**kwargs): 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]}) @@ -79,6 +83,7 @@ def shader_marble(nw: NodeWrangler,**kwargs): 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}) From d491e102fa96cfdde9469fe608b7b0be17dc126b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 405/727] Add 17 lines to infinigen/assets/materials/black_plastic.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/black_plastic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 infinigen/assets/materials/black_plastic.py diff --git a/infinigen/assets/materials/black_plastic.py b/infinigen/assets/materials/black_plastic.py new file mode 100644 index 000000000..d8b7bba88 --- /dev/null +++ b/infinigen/assets/materials/black_plastic.py @@ -0,0 +1,17 @@ +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}) From e385195445c3e2954883d14881d3f10fd1103db4 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 406/727] Add 5 lines to infinigen/assets/materials/black_plastic.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/materials/black_plastic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/materials/black_plastic.py b/infinigen/assets/materials/black_plastic.py index d8b7bba88..8a8a3a446 100644 --- a/infinigen/assets/materials/black_plastic.py +++ b/infinigen/assets/materials/black_plastic.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: 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 From 71c9f43c6cfa4b8fc8cd36bbe7d2c55c29938e3d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 407/727] Add 37 lines to infinigen/assets/materials/invisible_to_camera.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../assets/materials/invisible_to_camera.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 infinigen/assets/materials/invisible_to_camera.py 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 From 183b53b9134a3aad3c88b7ba5e2b6208b8c02a39 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 408/727] Add 10 lines to infinigen/assets/materials/fabrics.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/fabrics.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 infinigen/assets/materials/fabrics.py 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) From b24c82d13d6508b5517993ec43ff1aeb96a76ea2 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 409/727] Add 64 lines to infinigen/assets/materials/brick.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/brick.py | 64 +++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 infinigen/assets/materials/brick.py 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 From c45d4860f84dc5da462235ca87c3e211d8260f7b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 410/727] Add 150 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/table_marble.py | 150 +++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 infinigen/assets/materials/table_marble.py diff --git a/infinigen/assets/materials/table_marble.py b/infinigen/assets/materials/table_marble.py new file mode 100644 index 000000000..03cd47440 --- /dev/null +++ b/infinigen/assets/materials/table_marble.py @@ -0,0 +1,150 @@ +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, + 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}) From 9d273d15e0b1f734c85ca6add68c5e58dd0c211d Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 411/727] Add 7 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/materials/table_marble.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/materials/table_marble.py b/infinigen/assets/materials/table_marble.py index 03cd47440..9a77fd18a 100644 --- a/infinigen/assets/materials/table_marble.py +++ b/infinigen/assets/materials/table_marble.py @@ -1,3 +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: 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 d0dad68b5c4b0c539e896e63be358438c40fd88f Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 412/727] Add 1 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/materials/table_marble.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/table_marble.py b/infinigen/assets/materials/table_marble.py index 9a77fd18a..740f9ef75 100644 --- a/infinigen/assets/materials/table_marble.py +++ b/infinigen/assets/materials/table_marble.py @@ -65,6 +65,7 @@ def shader_marble(nw: NodeWrangler,**kwargs): 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]}) From f448f4ae278e242d7c0f17f9b6b6ca57f24b64e8 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 413/727] Add 53 lines to infinigen/assets/materials/marble_voronoi.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/assets/materials/marble_voronoi.py | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 infinigen/assets/materials/marble_voronoi.py 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 From 3573b6627c1781b28477ccfe453390ae3f21367f Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 414/727] Add 265 lines to infinigen/assets/materials/wear_tear/procedural_edge_wear.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../wear_tear/procedural_edge_wear.py | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 infinigen/assets/materials/wear_tear/procedural_edge_wear.py 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..6d2001089 --- /dev/null +++ b/infinigen/assets/materials/wear_tear/procedural_edge_wear.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: 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 + + +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 + + +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: + 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 + nw = NodeWrangler(material_node_tree) + + result = nw.find("ShaderNodeOutputMaterial") + if len(result) == 0: + 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']) + + return + +def apply(obj): + if not isinstance(obj, Iterable): + obj = [obj] + for o in obj: + apply_over(o) \ No newline at end of file From 27d0d138273dd11e779e29ca828b86a3b05bbb25 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 415/727] Add 11 lines to infinigen/assets/materials/wear_tear/procedural_edge_wear.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../materials/wear_tear/procedural_edge_wear.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/assets/materials/wear_tear/procedural_edge_wear.py b/infinigen/assets/materials/wear_tear/procedural_edge_wear.py index 6d2001089..1b4c44854 100644 --- a/infinigen/assets/materials/wear_tear/procedural_edge_wear.py +++ b/infinigen/assets/materials/wear_tear/procedural_edge_wear.py @@ -17,6 +17,7 @@ import logging from collections.abc import Iterable +logger = logging.getLogger(__name__) def get_edge_wear_params(): return { @@ -217,6 +218,8 @@ def shader_edge_tear_free_node_group(nw: NodeWrangler, return mix_shader_1, total_displacement +MARKER_LABEL = "wear_tear" + def apply_over(obj, selection=None, **shader_kwargs): # get all materials @@ -227,6 +230,7 @@ def apply_over(obj, selection=None, **shader_kwargs): return if len(shader_kwargs) == 0: + logging.debug("Obtaining Randomized Scratch Parameters") shader_kwargs = get_edge_wear_params() for material_name, material in materials: @@ -234,10 +238,15 @@ def apply_over(obj, selection=None, **shader_kwargs): # 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 @@ -255,6 +264,8 @@ def apply_over(obj, selection=None, **shader_kwargs): # 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 From 5ae9981fb20aca7af6ec90c90ff0627bd5cfc0b5 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 416/727] Add 145 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../materials/wear_tear/procedural_scratch.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 infinigen/assets/materials/wear_tear/procedural_scratch.py 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..51fc19165 --- /dev/null +++ b/infinigen/assets/materials/wear_tear/procedural_scratch.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: 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 + + +def get_scratch_params(): + return { + 'angle1': uniform(10., 80.0), + 'angle2': uniform(-80.0, -10.0), + } + +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 + return None + +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: + 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 + nw = NodeWrangler(material_node_tree) + + result = nw.find_recursive("ShaderNodeBsdf") + if len(result) == 0: + continue + + # final_bsdf = scratch_shader(nw_bsdf, bsdf, **shader_kwargs) + + + # connecting nodes: https://blender.stackexchange.com/questions/101820/how-to-add-remove-links-to-existing-or-new-nodes-using-python + +def apply(obj): + if not isinstance(obj, Iterable): + obj = [obj] + for o in obj: + apply_over(o) \ No newline at end of file From e5509c8dbf45611689e4222d24fbb51a219ddf83 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 417/727] Add 14 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../materials/wear_tear/procedural_scratch.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/infinigen/assets/materials/wear_tear/procedural_scratch.py b/infinigen/assets/materials/wear_tear/procedural_scratch.py index 51fc19165..3716057ff 100644 --- a/infinigen/assets/materials/wear_tear/procedural_scratch.py +++ b/infinigen/assets/materials/wear_tear/procedural_scratch.py @@ -17,6 +17,8 @@ import logging from collections.abc import Iterable +logger = logging.getLogger(__name__) + def get_scratch_params(): return { @@ -108,8 +110,11 @@ 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 @@ -120,6 +125,7 @@ def apply_over(obj, selection=None, **shader_kwargs): return if len(shader_kwargs) == 0: + logging.debug("Obtaining Randomized Scratch Parameters") shader_kwargs = get_scratch_params() for material_name, material in materials: @@ -127,10 +133,16 @@ def apply_over(obj, selection=None, **shader_kwargs): # 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 # final_bsdf = scratch_shader(nw_bsdf, bsdf, **shader_kwargs) @@ -138,6 +150,8 @@ def apply_over(obj, selection=None, **shader_kwargs): # connecting nodes: https://blender.stackexchange.com/questions/101820/how-to-add-remove-links-to-existing-or-new-nodes-using-python + nw_bsdf.label = MARKER_LABEL + def apply(obj): if not isinstance(obj, Iterable): obj = [obj] From fdd0a06fc757575a2ec3713ad00252db7410c30f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 418/727] Add 11 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/wear_tear/procedural_scratch.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/assets/materials/wear_tear/procedural_scratch.py b/infinigen/assets/materials/wear_tear/procedural_scratch.py index 3716057ff..2a3ebc5fc 100644 --- a/infinigen/assets/materials/wear_tear/procedural_scratch.py +++ b/infinigen/assets/materials/wear_tear/procedural_scratch.py @@ -17,6 +17,8 @@ import logging from collections.abc import Iterable +from infinigen.core.util.random import log_uniform + logger = logging.getLogger(__name__) @@ -24,6 +26,10 @@ 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, @@ -145,10 +151,15 @@ def apply_over(obj, selection=None, **shader_kwargs): 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 From 46a8136019aab6f60b84f198b4cddfe1eb11bb9f Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 419/727] Add 150 lines to infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../leather_and_fabrics/fine_knit_fabric.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py 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..d3512e30b --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.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: 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.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}, + ) + + + fabric_params = get_texture_params() + fabric_params["_map"] = "Object" + return shader_material(nw, **fabric_params) + + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_fabric_random, selection, **kwargs) From df363f7754b70ccf43b81c196f0a81924151a9d2 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 420/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/leather_and_fabrics/fine_knit_fabric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py b/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py index d3512e30b..d3d8f2dcf 100644 --- a/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py @@ -16,6 +16,8 @@ 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 @@ -141,10 +143,12 @@ def shader_material( ) +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) From b759490b02e871656e0808e469e6c368128d7b2a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 421/727] Add 36 lines to infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../leather_and_fabrics/sofa_fabric.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py 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..1e4a5a5cd --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.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: Lingjie Mei +from numpy.random import uniform + +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}) + unwrap_faces(obj, selection) + common.apply(obj, shader_sofa_fabric, selection, **kwargs) + From a33d5b12081cf9df46f8e4a0c7c23468515a117a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 422/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py b/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py index 1e4a5a5cd..03292592a 100644 --- a/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py @@ -4,6 +4,7 @@ # 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 @@ -17,6 +18,7 @@ def shader_sofa_fabric(nw: NodeWrangler, scale=1, **kwargs): 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, @@ -31,6 +33,8 @@ def shader_sofa_fabric(nw: NodeWrangler, scale=1, **kwargs): 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) From b6e10b8c5b367b70c83fd63ccd5c1bcd4979e432 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:24 -0700 Subject: [PATCH 423/727] Add 82 lines to infinigen/assets/materials/leather_and_fabrics/velvet.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- .../materials/leather_and_fabrics/velvet.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/velvet.py 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..30392b66c --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/velvet.py @@ -0,0 +1,82 @@ +# 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 + + + + # 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 From bbdd26f0b6ce069bd689cfc8bd71094cc7b90804 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 424/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/velvet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/leather_and_fabrics/velvet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/velvet.py b/infinigen/assets/materials/leather_and_fabrics/velvet.py index 30392b66c..3f7f8e2a4 100644 --- a/infinigen/assets/materials/leather_and_fabrics/velvet.py +++ b/infinigen/assets/materials/leather_and_fabrics/velvet.py @@ -1,3 +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: Stamatis Alexandropoulos # Acknowledgement: This file draws inspiration from https://www.youtube.com/watch?v=55MMAnTYhWI by Dikko @@ -12,6 +15,7 @@ +def shader_velvet(nw: NodeWrangler, **kwargs): # Code generated using version 2.6.5 of the node_transpiler texture_coordinate = nw.new_node(Nodes.TextureCoord) From 074197412f87b0794ea702aad079e6d392ab1232 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 425/727] Add 20 lines to infinigen/assets/materials/leather_and_fabrics/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/leather_and_fabrics/__init__.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/__init__.py 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..98dcf717e --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) Princeton University. +# This 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 + + + +def apply(obj, selection=None, **kwargs): + unwrap_faces(obj, selection) + common.apply(obj, rg(fabric_shader_list), selection=selection, **kwargs) From 065a5cd9a2a370fb52c201d9682dbe7f53b626c0 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 426/727] Add 2 lines to infinigen/assets/materials/leather_and_fabrics/__init__.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/leather_and_fabrics/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/__init__.py b/infinigen/assets/materials/leather_and_fabrics/__init__.py index 98dcf717e..751f12282 100644 --- a/infinigen/assets/materials/leather_and_fabrics/__init__.py +++ b/infinigen/assets/materials/leather_and_fabrics/__init__.py @@ -13,6 +13,8 @@ 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): From b8268dd77aff592259aa470eadd911f6f6e24cdd Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 427/727] Add 163 lines to infinigen/assets/materials/leather_and_fabrics/lined_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../leather_and_fabrics/lined_fabric.py | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/lined_fabric.py 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..82b357ae8 --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/lined_fabric.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: Meenal Parakh + + +import bpy +import mathutils +from numpy.random import uniform +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}, + ) + + + fabric_params = get_texture_params() + return shader_lined_fur_base(nw, **fabric_params) + + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_fabric_random, selection, **kwargs) From 9d4da7a8b9bbe0459ae5c104a649cd66b0f921aa Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 428/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/lined_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/leather_and_fabrics/lined_fabric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py b/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py index 82b357ae8..751d52479 100644 --- a/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/lined_fabric.py @@ -7,6 +7,8 @@ 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 @@ -155,9 +157,11 @@ def shader_lined_fur_base( ) +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) From 8494e35eb4284fdc8f07cef8826ed214df3cecd6 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 429/727] Add 125 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../leather_and_fabrics/general_fabric.py | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/general_fabric.py 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..cb906dd02 --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py @@ -0,0 +1,125 @@ +from infinigen.assets.materials import common + + +from infinigen.assets.utils.uv import ensure_uv, unwrap_faces + +def func_fabric(nw: NodeWrangler, **kwargs): + + 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'}) + + + + 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 + }) + + + 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'}) + + + 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'}) + + + 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'}) + + + mix_1 = nw.new_node(Nodes.Mix, input_kwargs={6: musgrave_texture, 7: mix.outputs[2]}, + attrs={'data_type': 'RGBA'}) + + + + displacement = nw.new_node( + 'ShaderNodeDisplacement', input_kwargs={'Height': multiply_1, 'Midlevel': 0.0000} + ) + + return {'Shader': mix_shader, 'Displacement': displacement} + + 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): + common.apply(obj, shader_fabric, selection, **kwargs) From b409649aa54e30250750f2fc0ae8cb109411b8ea Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 430/727] Add 41 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../leather_and_fabrics/general_fabric.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/general_fabric.py b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py index cb906dd02..e75733a2b 100644 --- a/infinigen/assets/materials/leather_and_fabrics/general_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py @@ -1,9 +1,29 @@ +# Copyright (c) Princeton University. +# This 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., @@ -20,7 +40,10 @@ def func_fabric(nw: NodeWrangler, **kwargs): '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"], @@ -29,13 +52,16 @@ def func_fabric(nw: NodeWrangler, **kwargs): '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'}) @@ -80,6 +106,7 @@ def func_fabric(nw: NodeWrangler, **kwargs): 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], @@ -94,11 +121,14 @@ def func_fabric(nw: NodeWrangler, **kwargs): 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} @@ -106,6 +136,17 @@ def func_fabric(nw: NodeWrangler, **kwargs): 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, From 81c4ba4c75eed4b2931a2975874ac8d820d149be Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 431/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../assets/materials/leather_and_fabrics/general_fabric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/general_fabric.py b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py index e75733a2b..33e1e987e 100644 --- a/infinigen/assets/materials/leather_and_fabrics/general_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/general_fabric.py @@ -163,4 +163,8 @@ def shader_fabric(nw: NodeWrangler, weave_scale=500.0, color_scale=None, color_1 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) From 1460edde109f9949c3e71a8278d9e2c064f18dbe Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 432/727] Add 74 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../materials/leather_and_fabrics/leather.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/leather.py 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..9976cafb7 --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/leather.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 +# 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 +from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler +from infinigen.core.nodes import node_utils +from infinigen.core import surface + +@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'}) + 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) + 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): + From 9d9a361d451bb34bfe2b8617a6af51a2a611ed6a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 433/727] Add 28 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/leather_and_fabrics/leather.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/leather.py b/infinigen/assets/materials/leather_and_fabrics/leather.py index 9976cafb7..8a1d72dee 100644 --- a/infinigen/assets/materials/leather_and_fabrics/leather.py +++ b/infinigen/assets/materials/leather_and_fabrics/leather.py @@ -9,8 +9,12 @@ import bpy import mathutils from numpy.random import uniform, normal, randint + +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 @node_utils.to_nodegroup('nodegroup_leather', singleton=False, type='ShaderNodeTree') @@ -18,43 +22,63 @@ 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}) @@ -65,10 +89,14 @@ def shader_leather(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kw seed = uniform(-1000.0, 1000.0) 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) From 07ca382edb523ccfedd9cc80b04d77da04df14ac Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 434/727] Add 8 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/materials/leather_and_fabrics/leather.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/leather.py b/infinigen/assets/materials/leather_and_fabrics/leather.py index 8a1d72dee..808f4a950 100644 --- a/infinigen/assets/materials/leather_and_fabrics/leather.py +++ b/infinigen/assets/materials/leather_and_fabrics/leather.py @@ -9,6 +9,7 @@ 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 @@ -16,6 +17,7 @@ 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): @@ -87,6 +89,11 @@ def shader_leather(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kw # 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}) @@ -96,6 +103,7 @@ def shader_leather(nw: NodeWrangler, scale=1.0, base_color=None, seed=None, **kw 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) From 07bd4691b35c5217b3c424403630538f355c8b19 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 435/727] Add 266 lines to infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../leather_and_fabrics/coarse_knit_fabric.py | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py 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..5d9dbb0d9 --- /dev/null +++ b/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py @@ -0,0 +1,266 @@ +# Copyright (c) Princeton University. +# This 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.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}, + ) + + + fabric_params = get_texture_params() + return shader_fabric_base(nw, **fabric_params) + + +def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_fabric_random, selection, **kwargs) From e78dc4fc0326edeb8efd22c4fd1462c0fe4f1a76 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 436/727] Add 4 lines to infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/leather_and_fabrics/coarse_knit_fabric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py b/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py index 5d9dbb0d9..0ac7179b0 100644 --- a/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py +++ b/infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py @@ -17,6 +17,8 @@ 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 @@ -258,9 +260,11 @@ def shader_fabric_base( ) +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) From 930d798d86444d7b8b6312616a66d12ea194e011 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 437/727] Add 35 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../materials/plastics/plastic_translucent.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 infinigen/assets/materials/plastics/plastic_translucent.py diff --git a/infinigen/assets/materials/plastics/plastic_translucent.py b/infinigen/assets/materials/plastics/plastic_translucent.py new file mode 100644 index 000000000..35f824207 --- /dev/null +++ b/infinigen/assets/materials/plastics/plastic_translucent.py @@ -0,0 +1,35 @@ +# 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. +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 + + # 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) + 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 From 83e7304141f22e97da754c8e78d76d29e7717be4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 438/727] Add 7 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/plastics/plastic_translucent.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/materials/plastics/plastic_translucent.py b/infinigen/assets/materials/plastics/plastic_translucent.py index 35f824207..f237f08b4 100644 --- a/infinigen/assets/materials/plastics/plastic_translucent.py +++ b/infinigen/assets/materials/plastics/plastic_translucent.py @@ -1,6 +1,7 @@ # 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 @@ -10,11 +11,17 @@ 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) From 10cbed429d33de5af5ba5376528abb3c48600593 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 439/727] Add 2 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/plastics/plastic_translucent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/materials/plastics/plastic_translucent.py b/infinigen/assets/materials/plastics/plastic_translucent.py index f237f08b4..d41a04264 100644 --- a/infinigen/assets/materials/plastics/plastic_translucent.py +++ b/infinigen/assets/materials/plastics/plastic_translucent.py @@ -1,7 +1,9 @@ # 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 47dc57625622d3faf29b288fcfece6b7f478c506 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 440/727] Add 69 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../materials/plastics/plastic_rough.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 infinigen/assets/materials/plastics/plastic_rough.py diff --git a/infinigen/assets/materials/plastics/plastic_rough.py b/infinigen/assets/materials/plastics/plastic_rough.py new file mode 100644 index 000000000..c00568791 --- /dev/null +++ b/infinigen/assets/materials/plastics/plastic_rough.py @@ -0,0 +1,69 @@ +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.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}) + + # 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: + + 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): From 787616317c2ebfc983e8222fdda5eb10c26efb91 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 441/727] Add 13 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/plastics/plastic_rough.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/materials/plastics/plastic_rough.py b/infinigen/assets/materials/plastics/plastic_rough.py index c00568791..e8091583c 100644 --- a/infinigen/assets/materials/plastics/plastic_rough.py +++ b/infinigen/assets/materials/plastics/plastic_rough.py @@ -1,7 +1,14 @@ +# 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 @@ -45,12 +52,17 @@ def nodegroup_plastics(nw: NodeWrangler): 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, @@ -67,3 +79,4 @@ def nodegroup_plastics(nw: NodeWrangler): def apply(obj, selection=None, **kwargs): + common.apply(obj, shader_rough_plastic, selection, **kwargs) \ No newline at end of file From 5e5f268a32ea544e9331e52c03a3572f34c98517 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 442/727] Add 1 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/plastics/plastic_rough.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/materials/plastics/plastic_rough.py b/infinigen/assets/materials/plastics/plastic_rough.py index e8091583c..c2db3bd0e 100644 --- a/infinigen/assets/materials/plastics/plastic_rough.py +++ b/infinigen/assets/materials/plastics/plastic_rough.py @@ -1,6 +1,7 @@ # 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 From 31421981747ebce1b5e516027518fb44e41f785e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 443/727] Add 49 lines to infinigen/assets/materials/metal/galvanized_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../materials/metal/galvanized_metal.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 infinigen/assets/materials/metal/galvanized_metal.py diff --git a/infinigen/assets/materials/metal/galvanized_metal.py b/infinigen/assets/materials/metal/galvanized_metal.py new file mode 100644 index 000000000..74fc0d594 --- /dev/null +++ b/infinigen/assets/materials/metal/galvanized_metal.py @@ -0,0 +1,49 @@ +# 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.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: + + 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): From 5784c06e83cd3a579067ddbb4c0093833119e830 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 444/727] Add 19 lines to infinigen/assets/materials/metal/galvanized_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/metal/galvanized_metal.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/infinigen/assets/materials/metal/galvanized_metal.py b/infinigen/assets/materials/metal/galvanized_metal.py index 74fc0d594..ddf6a4040 100644 --- a/infinigen/assets/materials/metal/galvanized_metal.py +++ b/infinigen/assets/materials/metal/galvanized_metal.py @@ -1,3 +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: Yiming Zuo # Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=ECl2pQ1jQm8 by Ryan King Art @@ -5,11 +8,14 @@ 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 @@ -18,21 +24,30 @@ def nodegroup_galvanized_metal(nw: NodeWrangler): 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}) @@ -41,9 +56,13 @@ def shader_galvanized_metal(nw: NodeWrangler, scale=1.0, base_color=None, seed=N 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) From 54ad94f3d30ef5e9a10db2b2de71fdc1a606bcf4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 445/727] Add 33 lines to infinigen/assets/materials/metal/metal_basic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/metal/metal_basic.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 infinigen/assets/materials/metal/metal_basic.py 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) From ecf01d93bf4ab41478052d09dac9b5b81e6448fc Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 446/727] Add 55 lines to infinigen/assets/materials/metal/hammered_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/materials/metal/hammered_metal.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 infinigen/assets/materials/metal/hammered_metal.py diff --git a/infinigen/assets/materials/metal/hammered_metal.py b/infinigen/assets/materials/metal/hammered_metal.py new file mode 100644 index 000000000..7b88ad0b8 --- /dev/null +++ b/infinigen/assets/materials/metal/hammered_metal.py @@ -0,0 +1,55 @@ +# 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 + +@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) + 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'}) + 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}) + + # 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: + + 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): From c0e6f8d06da596d1ffca54de17486333b7681d95 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 447/727] Add 26 lines to infinigen/assets/materials/metal/hammered_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/metal/hammered_metal.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/infinigen/assets/materials/metal/hammered_metal.py b/infinigen/assets/materials/metal/hammered_metal.py index 7b88ad0b8..d87a27f2b 100644 --- a/infinigen/assets/materials/metal/hammered_metal.py +++ b/infinigen/assets/materials/metal/hammered_metal.py @@ -1,3 +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: Yiming Zuo # Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=82smQvoh0GE by Mix CG Arts @@ -9,6 +12,8 @@ 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): @@ -18,38 +23,59 @@ def nodegroup_hammered_metal(nw: NodeWrangler): 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) From 1a6bb9d39d65def68bc9b6a43277f2a584e989b7 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 448/727] Add 56 lines to infinigen/assets/materials/metal/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/metal/__init__.py | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 infinigen/assets/materials/metal/__init__.py 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)) From e881d698adc6e7c1821a833c903b9cc33aa1d739 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 449/727] Add 59 lines to infinigen/assets/materials/metal/grained_and_polished_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../metal/grained_and_polished_metal.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 infinigen/assets/materials/metal/grained_and_polished_metal.py 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..a04468b06 --- /dev/null +++ b/infinigen/assets/materials/metal/grained_and_polished_metal.py @@ -0,0 +1,59 @@ +# 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'}) + 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: + + + 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): From bbd950acc9db46d0757e45a4c2a8186cdf0ffedb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:25 -0700 Subject: [PATCH 450/727] Add 20 lines to infinigen/assets/materials/metal/grained_and_polished_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../metal/grained_and_polished_metal.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/infinigen/assets/materials/metal/grained_and_polished_metal.py b/infinigen/assets/materials/metal/grained_and_polished_metal.py index a04468b06..5e63f8219 100644 --- a/infinigen/assets/materials/metal/grained_and_polished_metal.py +++ b/infinigen/assets/materials/metal/grained_and_polished_metal.py @@ -1,3 +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: Yiming Zuo import bpy @@ -9,6 +12,7 @@ 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 @@ -18,20 +22,30 @@ def nodegroup_grained_metal(nw: NodeWrangler): ('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}) @@ -44,16 +58,22 @@ def shader_grained_metal(nw: NodeWrangler, scale=1.0, base_color=None, roughness 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) From a44b7cd225c1cc17d5d719019707afc0e7ab9e6e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 451/727] Add 67 lines to infinigen/assets/materials/metal/brushed_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/materials/metal/brushed_metal.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 infinigen/assets/materials/metal/brushed_metal.py diff --git a/infinigen/assets/materials/metal/brushed_metal.py b/infinigen/assets/materials/metal/brushed_metal.py new file mode 100644 index 000000000..992c55bd6 --- /dev/null +++ b/infinigen/assets/materials/metal/brushed_metal.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: 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: + 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): From 5fae6358fc60adfd49534d79ba908bb3ad0099f3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 452/727] Add 20 lines to infinigen/assets/materials/metal/brushed_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/metal/brushed_metal.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/infinigen/assets/materials/metal/brushed_metal.py b/infinigen/assets/materials/metal/brushed_metal.py index 992c55bd6..18d879394 100644 --- a/infinigen/assets/materials/metal/brushed_metal.py +++ b/infinigen/assets/materials/metal/brushed_metal.py @@ -14,41 +14,56 @@ 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}) @@ -58,10 +73,15 @@ def shader_brushed_metal(nw: NodeWrangler, scale=1.0, base_color=None, seed=None 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) From 66c18fb7a556b68984a6ad34d8db12058dcc5322 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 453/727] Add 179 lines to infinigen/assets/materials/stone_and_concrete/concrete.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../materials/stone_and_concrete/concrete.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 infinigen/assets/materials/stone_and_concrete/concrete.py 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..62aacfb4c --- /dev/null +++ b/infinigen/assets/materials/stone_and_concrete/concrete.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: 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.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}) + + 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, + '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): From 2c8273271ef46c37bc41a880130e8a3eeab7d3bf Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 454/727] Add 63 lines to infinigen/assets/materials/stone_and_concrete/concrete.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/stone_and_concrete/concrete.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/infinigen/assets/materials/stone_and_concrete/concrete.py b/infinigen/assets/materials/stone_and_concrete/concrete.py index 62aacfb4c..a41f00ec6 100644 --- a/infinigen/assets/materials/stone_and_concrete/concrete.py +++ b/infinigen/assets/materials/stone_and_concrete/concrete.py @@ -9,6 +9,8 @@ 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 @@ -23,46 +25,66 @@ def nodegroup_crack(nw: NodeWrangler): ('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') @@ -77,82 +99,118 @@ def nodegroup_concrete(nw: NodeWrangler): ('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: @@ -169,11 +227,16 @@ def nodegroup_concrete(nw: NodeWrangler): 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) From 5f0f5e850b7946611c52c24370e16d6b8af9ae4a Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 455/727] Add 116 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/materials/woods/tiled_wood.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 infinigen/assets/materials/woods/tiled_wood.py diff --git a/infinigen/assets/materials/woods/tiled_wood.py b/infinigen/assets/materials/woods/tiled_wood.py new file mode 100644 index 000000000..edbea6494 --- /dev/null +++ b/infinigen/assets/materials/woods/tiled_wood.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: Yiming Zuo +# Acknowledgement: This file draws inspiration https://www.youtube.com/watch?v=rd2jhGV6tqo by Ryan King Art + + +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 + +@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}) + 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, + 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', + 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'}) + 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: + 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: + + group = nw.new_node(nodegroup_tiled_wood().name, + '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 apply(obj, selection=None, **kwargs): From 4ee10ef478a112cad7f05010e8a68dcd1a3c466f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 456/727] Add 58 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/tiled_wood.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/infinigen/assets/materials/woods/tiled_wood.py b/infinigen/assets/materials/woods/tiled_wood.py index edbea6494..ae64cb550 100644 --- a/infinigen/assets/materials/woods/tiled_wood.py +++ b/infinigen/assets/materials/woods/tiled_wood.py @@ -8,32 +8,53 @@ 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 + @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}) @@ -43,55 +64,82 @@ 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', + 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}) @@ -105,12 +153,22 @@ def shader_wood_tiled(nw: NodeWrangler, hscale=None, vscale=None, base_color=Non 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 apply(obj, selection=None, **kwargs): + common.apply(obj, shader_wood_tiled, selection=selection, **kwargs) + +# def make_sphere(): +# return new_plane() From 8a31e1f8f5b25697afa3f069d15baa79150b05a6 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 457/727] Add 11 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/materials/woods/tiled_wood.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/assets/materials/woods/tiled_wood.py b/infinigen/assets/materials/woods/tiled_wood.py index ae64cb550..d8beedfaf 100644 --- a/infinigen/assets/materials/woods/tiled_wood.py +++ b/infinigen/assets/materials/woods/tiled_wood.py @@ -17,6 +17,8 @@ from infinigen.core.nodes import node_utils from infinigen.core.util.color import rgb2hsv +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): @@ -121,6 +123,7 @@ def nodegroup_tiled_wood(nw: NodeWrangler): 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"]}, @@ -144,6 +147,7 @@ def nodegroup_tiled_wood(nw: NodeWrangler): 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 @@ -167,6 +171,13 @@ def shader_wood_tiled(nw: NodeWrangler, hscale=None, vscale=None, base_color=Non 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) From 4ff917656955a8c41ae6ede2768e49c879f2e84f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 458/727] Add 3 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/woods/tiled_wood.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/materials/woods/tiled_wood.py b/infinigen/assets/materials/woods/tiled_wood.py index d8beedfaf..3cc818923 100644 --- a/infinigen/assets/materials/woods/tiled_wood.py +++ b/infinigen/assets/materials/woods/tiled_wood.py @@ -17,9 +17,11 @@ 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 @@ -152,6 +154,7 @@ def shader_wood_tiled(nw: NodeWrangler, hscale=None, vscale=None, base_color=Non # 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: From 2d444c340a836a4f788f9aa5e3eb82fa143e8a7b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 459/727] Add 14 lines to infinigen/assets/materials/woods/crossed_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/crossed_wood_tile.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 infinigen/assets/materials/woods/crossed_wood_tile.py 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 From 95ddc22b969fc5a1014c7b4e8a73894154322f3a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 460/727] Add 12 lines to infinigen/assets/materials/woods/staggered_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/staggered_wood_tile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infinigen/assets/materials/woods/staggered_wood_tile.py 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) From 24805d25d40ddc1df476f00bf0228c7c395fda9c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 461/727] Add 133 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/woods/wood.py | 133 +++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 infinigen/assets/materials/woods/wood.py diff --git a/infinigen/assets/materials/woods/wood.py b/infinigen/assets/materials/woods/wood.py new file mode 100644 index 000000000..e4ce6d3c7 --- /dev/null +++ b/infinigen/assets/materials/woods/wood.py @@ -0,0 +1,133 @@ +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): + r = uniform() + if r < 1 / 12: + elif r < 2 / 12: + elif r < 3 / 12: + else: + shader = shader_wood + common.apply(obj, shader, selection, **kwargs) + +def make_sphere(): + return new_cube() From 646b4ec30d611817ce5293c5e2a8e86ec7687986 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 462/727] Add 7 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/materials/woods/wood.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/materials/woods/wood.py b/infinigen/assets/materials/woods/wood.py index e4ce6d3c7..c01802023 100644 --- a/infinigen/assets/materials/woods/wood.py +++ b/infinigen/assets/materials/woods/wood.py @@ -121,10 +121,17 @@ def shader_wood(nw: NodeWrangler, color=None, w=None, vertical=False, **kwargs): 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) From a3d1a13abfc9f1e953b95f21eddc7f2ce85de0af Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 463/727] Add 6 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/materials/woods/wood.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/materials/woods/wood.py b/infinigen/assets/materials/woods/wood.py index c01802023..1891ab51d 100644 --- a/infinigen/assets/materials/woods/wood.py +++ b/infinigen/assets/materials/woods/wood.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: 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 fd6cb39d2efc9186e7b7075a1d0e5469a0c96740 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 464/727] Add 14 lines to infinigen/assets/materials/woods/square_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/square_wood_tile.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 infinigen/assets/materials/woods/square_wood_tile.py 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 From e8728a7544edac98c692fe9e32772dfb02790a27 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 465/727] Add 22 lines to infinigen/assets/materials/woods/non_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/non_wood_tile.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 infinigen/assets/materials/woods/non_wood_tile.py 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) From 0e02b34e068cf06ad16cd0812a618ca7c471577c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 466/727] Add 18 lines to infinigen/assets/materials/woods/composite_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../materials/woods/composite_wood_tile.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 infinigen/assets/materials/woods/composite_wood_tile.py 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) From f5d1fbb6d863ff25ea7f68f2df2b2df1f5a155fb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 467/727] Add 12 lines to infinigen/assets/materials/woods/hexagon_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/materials/woods/hexagon_wood_tile.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 infinigen/assets/materials/woods/hexagon_wood_tile.py 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) From c2603d5aa476e0e37ca03865971a09b833b14d9c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 468/727] Add 15 lines to infinigen/assets/materials/woods/wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/woods/wood_tile.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 infinigen/assets/materials/woods/wood_tile.py 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) From ce269a28459efc6cf208a41f587c24df74700068 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 469/727] Add 50 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/materials/woods/wood_old.py | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 infinigen/assets/materials/woods/wood_old.py diff --git a/infinigen/assets/materials/woods/wood_old.py b/infinigen/assets/materials/woods/wood_old.py new file mode 100644 index 000000000..be45ba66f --- /dev/null +++ b/infinigen/assets/materials/woods/wood_old.py @@ -0,0 +1,50 @@ +# Authors: Mingzhe Wang, Lingjie Mei + +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.util.random import log_uniform + +def shader_wood_old(nw: NodeWrangler, scale=1, offset=None, rotation=None, **kwargs): + + 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) + + 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[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"]}) + 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) From 3fa3ebf3e8bbacfb3321e3f2e2404423923fccd7 Mon Sep 17 00:00:00 2001 From: Mingzhe Wang Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 470/727] Add 21 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Mingzhe Wang. --- infinigen/assets/materials/woods/wood_old.py | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/infinigen/assets/materials/woods/wood_old.py b/infinigen/assets/materials/woods/wood_old.py index be45ba66f..a155b23e5 100644 --- a/infinigen/assets/materials/woods/wood_old.py +++ b/infinigen/assets/materials/woods/wood_old.py @@ -1,4 +1,5 @@ # Authors: Mingzhe Wang, Lingjie Mei +import math as ma import numpy as np @@ -9,7 +10,11 @@ from infinigen.core import surface 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={ @@ -28,23 +33,39 @@ def shader_wood_old(nw: NodeWrangler, scale=1, offset=None, rotation=None, **kwa 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) From 3f8325a19b788b9f6f40eec99ab73e8faa9f11aa Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 471/727] Add 3 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/materials/woods/wood_old.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/materials/woods/wood_old.py b/infinigen/assets/materials/woods/wood_old.py index a155b23e5..193137bf0 100644 --- a/infinigen/assets/materials/woods/wood_old.py +++ b/infinigen/assets/materials/woods/wood_old.py @@ -1,3 +1,5 @@ +# Copyright (c) Princeton University. + # Authors: Mingzhe Wang, Lingjie Mei import math as ma @@ -8,6 +10,7 @@ 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 From d8f90636884d9f6091082add786379ff077d22eb Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 472/727] Add 2 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/materials/woods/wood_old.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/materials/woods/wood_old.py b/infinigen/assets/materials/woods/wood_old.py index 193137bf0..b1c2d2e49 100644 --- a/infinigen/assets/materials/woods/wood_old.py +++ b/infinigen/assets/materials/woods/wood_old.py @@ -1,6 +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: Mingzhe Wang, Lingjie Mei + import math as ma import numpy as np From debfb9cf8743662f50c0d34a13bf54ef27568908 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 473/727] Add 60 lines to infinigen/assets/clothes/pants.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/clothes/pants.py | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 infinigen/assets/clothes/pants.py diff --git a/infinigen/assets/clothes/pants.py b/infinigen/assets/clothes/pants.py new file mode 100644 index 000000000..866c9bab0 --- /dev/null +++ b/infinigen/assets/clothes/pants.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: 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 + + +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) + + 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 From cef04466b840174aaf224d799361358dadcb3c8a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 474/727] Add 6 lines to infinigen/assets/clothes/pants.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/clothes/pants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/clothes/pants.py b/infinigen/assets/clothes/pants.py index 866c9bab0..848f94980 100644 --- a/infinigen/assets/clothes/pants.py +++ b/infinigen/assets/clothes/pants.py @@ -14,6 +14,8 @@ 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): @@ -31,6 +33,10 @@ def __init__(self, factory_seed, coarse=False): 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 * ( From dd93a81e4e489aee642353fdbef8dd33d4c09939 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 475/727] Add 73 lines to infinigen/assets/clothes/blanket.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/clothes/blanket.py | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 infinigen/assets/clothes/blanket.py diff --git a/infinigen/assets/clothes/blanket.py b/infinigen/assets/clothes/blanket.py new file mode 100644 index 000000000..154753f76 --- /dev/null +++ b/infinigen/assets/clothes/blanket.py @@ -0,0 +1,73 @@ +# Copyright (c) Princeton University. +# This 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 + +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) + + + + 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 From b7d61a49517a8a85fe4af00804d7cd0eb7e6c914 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 476/727] Add 6 lines to infinigen/assets/clothes/blanket.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/clothes/blanket.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/clothes/blanket.py b/infinigen/assets/clothes/blanket.py index 154753f76..2d5d018b7 100644 --- a/infinigen/assets/clothes/blanket.py +++ b/infinigen/assets/clothes/blanket.py @@ -13,6 +13,8 @@ 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): @@ -21,6 +23,10 @@ def __init__(self, factory_seed, coarse=False): 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: From 870c3b3694aaefb2e829cc1559d3ca912f3ee747 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 477/727] Add 117 lines to infinigen/assets/clothes/towel.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/clothes/towel.py | 117 ++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 infinigen/assets/clothes/towel.py diff --git a/infinigen/assets/clothes/towel.py b/infinigen/assets/clothes/towel.py new file mode 100644 index 000000000..2c8449379 --- /dev/null +++ b/infinigen/assets/clothes/towel.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 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 + + +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() + 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 From c0d8c1b1a9dd8abb5743e1bbcee78beecf56c646 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 478/727] Add 6 lines to infinigen/assets/clothes/towel.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/clothes/towel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/clothes/towel.py b/infinigen/assets/clothes/towel.py index 2c8449379..964191c44 100644 --- a/infinigen/assets/clothes/towel.py +++ b/infinigen/assets/clothes/towel.py @@ -19,6 +19,7 @@ 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): @@ -34,6 +35,11 @@ def __init__(self, factory_seed, coarse=False): 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): From 7ba9172b41046ed8f3b3fd0ee1ff1800040ddfab Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:26 -0700 Subject: [PATCH 479/727] Add 9 lines to infinigen/assets/clothes/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/clothes/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 infinigen/assets/clothes/__init__.py 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 + From 7ca12836d653404c7e17f69a859c9d45c12a3808 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 480/727] Add 65 lines to infinigen/assets/clothes/shirt.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/clothes/shirt.py | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 infinigen/assets/clothes/shirt.py diff --git a/infinigen/assets/clothes/shirt.py b/infinigen/assets/clothes/shirt.py new file mode 100644 index 000000000..99316352b --- /dev/null +++ b/infinigen/assets/clothes/shirt.py @@ -0,0 +1,65 @@ +# Copyright (c) Princeton University. +# This 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 + +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) + + 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 From 42fb9c9cbd548ef734737dd8710114285b43ca91 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 481/727] Add 6 lines to infinigen/assets/clothes/shirt.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/clothes/shirt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/clothes/shirt.py b/infinigen/assets/clothes/shirt.py index 99316352b..9c5b1c7ec 100644 --- a/infinigen/assets/clothes/shirt.py +++ b/infinigen/assets/clothes/shirt.py @@ -13,6 +13,8 @@ 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): @@ -29,6 +31,10 @@ def __init__(self, factory_seed, coarse=False): 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( From 3ee496e53b4f23f2f7739f766d95d0f06f3e3903 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 482/727] Add 138 lines to infinigen/assets/bathroom/bathroom_sink.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/bathroom/bathroom_sink.py | 138 +++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 infinigen/assets/bathroom/bathroom_sink.py diff --git a/infinigen/assets/bathroom/bathroom_sink.py b/infinigen/assets/bathroom/bathroom_sink.py new file mode 100644 index 000000000..eb914951d --- /dev/null +++ b/infinigen/assets/bathroom/bathroom_sink.py @@ -0,0 +1,138 @@ +# Copyright (c) Princeton University. +# This 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 + + +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): + + +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 From 5bceab8027bf53d73949b846e16be4689fba3b00 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 483/727] Add 5 lines to infinigen/assets/bathroom/bathroom_sink.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/bathroom/bathroom_sink.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/bathroom/bathroom_sink.py b/infinigen/assets/bathroom/bathroom_sink.py index eb914951d..c1474d254 100644 --- a/infinigen/assets/bathroom/bathroom_sink.py +++ b/infinigen/assets/bathroom/bathroom_sink.py @@ -14,6 +14,7 @@ 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): @@ -128,6 +129,10 @@ def add_stand(self, 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 StandingSinkFactory(BathroomSinkFactory): From dcc1a88d1d13eae589004ad327abc6e715e69481 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 484/727] Add 8 lines to infinigen/assets/bathroom/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/bathroom/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 infinigen/assets/bathroom/__init__.py 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 From 6c88869374d5ced2ef1673a758ba09519781b423 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 485/727] Add 236 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/bathroom/bathtub.py | 236 +++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 infinigen/assets/bathroom/bathtub.py diff --git a/infinigen/assets/bathroom/bathtub.py b/infinigen/assets/bathroom/bathtub.py new file mode 100644 index 000000000..d5b6a22ad --- /dev/null +++ b/infinigen/assets/bathroom/bathtub.py @@ -0,0 +1,236 @@ +# Copyright (c) Princeton University. +# This 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 + + +class BathtubFactory(AssetFactory): + def __init__(self, factory_seed, coarse=False): + super(BathtubFactory, self).__init__(factory_seed, coarse) + self.bathtub_type = np.random.choice(['alcove', 'freestanding'], p=prob / prob.sum()) # , 'corner' + # /////////////////// assign materials /////////////////// + # //////////////////////////////////////////////////////// + + @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: + + 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) + + 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') + self.edge_wear.apply(assets) From e01330d32f55c8e4e79c2f3f6915082c55a95511 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 486/727] Add 40 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/bathroom/bathtub.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/infinigen/assets/bathroom/bathtub.py b/infinigen/assets/bathroom/bathtub.py index d5b6a22ad..f46396977 100644 --- a/infinigen/assets/bathroom/bathtub.py +++ b/infinigen/assets/bathroom/bathtub.py @@ -17,15 +17,49 @@ 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 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 /////////////////// # //////////////////////////////////////////////////////// + self.beveler = BevelSharp(mult=5, segments=5) + @property def has_base(self): return self.bathtub_type != 'freestanding' @@ -35,6 +69,7 @@ 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: @@ -57,6 +92,11 @@ def create_asset(self, **params) -> bpy.types.Object: 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): From df99a303f808dbdd67bff91991077d2a4db1fa04 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 487/727] Add 13 lines to infinigen/assets/bathroom/bathtub.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/bathroom/bathtub.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/bathroom/bathtub.py b/infinigen/assets/bathroom/bathtub.py index f46396977..1d6df5012 100644 --- a/infinigen/assets/bathroom/bathtub.py +++ b/infinigen/assets/bathroom/bathtub.py @@ -20,6 +20,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.assets.utils.autobevel import BevelSharp +from infinigen.assets.material_assignments import AssetList class BathtubFactory(AssetFactory): @@ -56,6 +57,14 @@ def __init__(self, factory_seed, coarse=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) @@ -273,4 +282,8 @@ def finalize_assets(self, assets): 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) From 0d18024cdf0a317eaa390f6011a591309dbbfcd9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 488/727] Add 110 lines to infinigen/assets/bathroom/hardware.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/bathroom/hardware.py | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 infinigen/assets/bathroom/hardware.py diff --git a/infinigen/assets/bathroom/hardware.py b/infinigen/assets/bathroom/hardware.py new file mode 100644 index 000000000..86cdd4614 --- /dev/null +++ b/infinigen/assets/bathroom/hardware.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: 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 + + +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 + + 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') + self.edge_wear.apply(assets) From 2871576fc1f2c1aff9394eef0bbf48f42ccd856d Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 489/727] Add 11 lines to infinigen/assets/bathroom/hardware.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/bathroom/hardware.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/assets/bathroom/hardware.py b/infinigen/assets/bathroom/hardware.py index 86cdd4614..411dcbbda 100644 --- a/infinigen/assets/bathroom/hardware.py +++ b/infinigen/assets/bathroom/hardware.py @@ -12,6 +12,7 @@ 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): @@ -30,6 +31,13 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -107,4 +115,7 @@ def create_asset(self, **params) -> bpy.types.Object: 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) From 236a01125fcbc7c37ee741814ee3c52b4795bb0d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 490/727] Add 286 lines to infinigen/assets/bathroom/toilet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/bathroom/toilet.py | 286 ++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 infinigen/assets/bathroom/toilet.py diff --git a/infinigen/assets/bathroom/toilet.py b/infinigen/assets/bathroom/toilet.py new file mode 100644 index 000000000..a5599bc96 --- /dev/null +++ b/infinigen/assets/bathroom/toilet.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: 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 + + +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') From 1202387ced365bb9ce15aa8098b162413b6e34f7 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 491/727] Add 6 lines to infinigen/assets/bathroom/toilet.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/bathroom/toilet.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/bathroom/toilet.py b/infinigen/assets/bathroom/toilet.py index a5599bc96..2e82f638e 100644 --- a/infinigen/assets/bathroom/toilet.py +++ b/infinigen/assets/bathroom/toilet.py @@ -17,6 +17,7 @@ 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): @@ -55,6 +56,7 @@ def __init__(self, factory_seed, coarse=False): 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 @@ -284,3 +286,7 @@ def add_handle(self): 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) From e1847db16a13319403b352dd307332a4220676ac Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 492/727] Add 850 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/cell_shelf.py | 850 +++++++++++++++++++++++++ 1 file changed, 850 insertions(+) create mode 100644 infinigen/assets/shelves/cell_shelf.py diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py new file mode 100644 index 000000000..7ac939577 --- /dev/null +++ b/infinigen/assets/shelves/cell_shelf.py @@ -0,0 +1,850 @@ +# Copyright (c) Princeton University. +# This 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 + +from infinigen.assets.shelves.utils import nodegroup_tagged_cube + 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 + + 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, + 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): + super(CellShelfBaseFactory, self).__init__(factory_seed, coarse=coarse) + + + 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['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 + + + 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 + return params From cdd4944120bdb9d4577d836c357e429d4a6655bc Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 493/727] Add 36 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/cell_shelf.py | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py index 7ac939577..4ae56dd74 100644 --- a/infinigen/assets/shelves/cell_shelf.py +++ b/infinigen/assets/shelves/cell_shelf.py @@ -2,12 +2,17 @@ # This 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.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.assets.shelves.utils import nodegroup_tagged_cube shader_shelves_white, shader_shelves_white_sampler, shader_shelves_black_wood, shader_shelves_black_wood_sampler, @@ -755,8 +760,15 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): class CellShelfBaseFactory(AssetFactory): + def __init__(self, factory_seed, coarse=False): super(CellShelfBaseFactory, self).__init__(factory_seed, coarse=coarse) + 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) @@ -788,6 +800,7 @@ class CellShelfBaseFactory(AssetFactory): 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 @@ -831,11 +844,14 @@ def create_asset(self, i=0, **params): 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=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), @@ -848,3 +864,23 @@ def sample_params(self): params['depth'] = params['Dimensions'][0] params['has_base_frame'] = False return params + 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 From 1b2ccbe7ef3f316ff444cf36871a39beb5969017 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 494/727] Add 9 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/shelves/cell_shelf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py index 4ae56dd74..8bb6a249e 100644 --- a/infinigen/assets/shelves/cell_shelf.py +++ b/infinigen/assets/shelves/cell_shelf.py @@ -20,6 +20,8 @@ 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): @@ -762,6 +764,7 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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) @@ -844,6 +847,7 @@ def create_asset(self, i=0, **params): 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) @@ -863,7 +867,12 @@ def sample_params(self): 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): From b7467ad8291424981582739b24f4d0cc9e7d62c7 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 495/727] Add 3 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/cell_shelf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py index 8bb6a249e..eef7d5841 100644 --- a/infinigen/assets/shelves/cell_shelf.py +++ b/infinigen/assets/shelves/cell_shelf.py @@ -6,6 +6,8 @@ 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 @@ -747,6 +749,7 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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, From b6f1decf59db027331f2d94acede56c4528eede5 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 496/727] Add 2 lines to infinigen/assets/shelves/cell_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/cell_shelf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/cell_shelf.py b/infinigen/assets/shelves/cell_shelf.py index eef7d5841..781e36496 100644 --- a/infinigen/assets/shelves/cell_shelf.py +++ b/infinigen/assets/shelves/cell_shelf.py @@ -14,8 +14,10 @@ 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, From bca0578391fffe8e5274e43be77462140711fffa Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 497/727] Add 410 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/drawers.py | 410 ++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 infinigen/assets/shelves/drawers.py diff --git a/infinigen/assets/shelves/drawers.py b/infinigen/assets/shelves/drawers.py new file mode 100644 index 000000000..0de8d8bf4 --- /dev/null +++ b/infinigen/assets/shelves/drawers.py @@ -0,0 +1,410 @@ +# Copyright (c) Princeton University. +# This 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 + +import bpy + shader_shelves_white, shader_shelves_white_sampler, + shader_shelves_black_wood, shader_shelves_black_wood_sampler, + shader_shelves_wood, shader_shelves_wood_sampler, + + +@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': + 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 From 539ce9b77042b0afa2b22b5d53da10fef6d6d97c Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 498/727] Add 4 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/drawers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/shelves/drawers.py b/infinigen/assets/shelves/drawers.py index 0de8d8bf4..debaa4d3d 100644 --- a/infinigen/assets/shelves/drawers.py +++ b/infinigen/assets/shelves/drawers.py @@ -4,6 +4,8 @@ # 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 @@ -15,6 +17,7 @@ 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') @@ -391,6 +394,7 @@ def get_material_func(self, params, randomness=True): 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'] From f69f4eaeaed32eabd3b0d0744a317bb2c30b745b Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 499/727] Add 1 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/drawers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/drawers.py b/infinigen/assets/shelves/drawers.py index debaa4d3d..c64fb57e7 100644 --- a/infinigen/assets/shelves/drawers.py +++ b/infinigen/assets/shelves/drawers.py @@ -14,6 +14,7 @@ from infinigen.core.util import blender as butil 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, From 81fb138494a2ed9dbf23e396ac23e1b8940f95ea Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 500/727] Add 1 lines to infinigen/assets/shelves/drawers.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/drawers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/drawers.py b/infinigen/assets/shelves/drawers.py index c64fb57e7..7393f9819 100644 --- a/infinigen/assets/shelves/drawers.py +++ b/infinigen/assets/shelves/drawers.py @@ -12,6 +12,7 @@ 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 ( From 8a6bacc17861fe3004f849bd18c1ad98245013f1 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 501/727] Add 356 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/simple_bookcase.py | 356 ++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 infinigen/assets/shelves/simple_bookcase.py diff --git a/infinigen/assets/shelves/simple_bookcase.py b/infinigen/assets/shelves/simple_bookcase.py new file mode 100644 index 000000000..9e1073c18 --- /dev/null +++ b/infinigen/assets/shelves/simple_bookcase.py @@ -0,0 +1,356 @@ +# Copyright (c) Princeton University. +# This 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 + +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 + + + 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'}) + + + 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}) + + + 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'}) + + + 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}) + + 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 + + + + + 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'}) + + attrs={'operation': 'SUBTRACT'}) + + combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': subtract, 'Z': subtract_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}) + + + + + 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}) + + + combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply, 'Y': multiply_3, 'Z': add_1}) + + + + 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 + + + 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}) + + + 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 + + + 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}) + + + if tag_support: + cube_1 = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz_3}) + else: + + +@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 + + + 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}) + + 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}) + + + +@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 + + + 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'] + + + 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, + + backboard_thickness = nw.new_node(Nodes.Value, label='backboard_thickness') + backboard_thickness.outputs[0].default_value = kwargs['backboard_thickness'] + + + + realize_instances = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry}) + + + 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'] + + + realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': screw_head}) + + + 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'] + + + + realize_instances_2 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': join_geometry_2}) + + + 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}) + + + 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['tag_support'] = True + return params + + def create_asset(self, i=0, **params): + 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) + + return obj + + +class SimpleBookcaseFactory(SimpleBookcaseBaseFactory): + def sample_params(self): + params = dict() + params['depth'] = params['Dimensions'][0] - 0.015 + params['width'] = params['Dimensions'][1] + params['height'] = params['Dimensions'][2] + return params From aabd2b882d671664e09a69bc57f4b56389fee05e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 502/727] Add 159 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/simple_bookcase.py | 159 ++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/infinigen/assets/shelves/simple_bookcase.py b/infinigen/assets/shelves/simple_bookcase.py index 9e1073c18..d88e22a4e 100644 --- a/infinigen/assets/shelves/simple_bookcase.py +++ b/infinigen/assets/shelves/simple_bookcase.py @@ -4,6 +4,7 @@ # 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 @@ -19,6 +20,10 @@ 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}) @@ -37,6 +42,10 @@ def nodegroup_attach_gadget(nw: NodeWrangler): 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}) @@ -46,16 +55,25 @@ def nodegroup_attach_gadget(nw: NodeWrangler): 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}) @@ -63,8 +81,24 @@ def nodegroup_attach_gadget(nw: NodeWrangler): 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'}) @@ -76,10 +110,13 @@ def nodegroup_screw_head(nw: NodeWrangler): 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"]}) @@ -87,8 +124,14 @@ def nodegroup_screw_head(nw: NodeWrangler): 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}) @@ -96,10 +139,17 @@ def nodegroup_screw_head(nw: NodeWrangler): 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)}) @@ -114,6 +164,9 @@ def nodegroup_screw_head(nw: NodeWrangler): 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}) @@ -122,6 +175,12 @@ def nodegroup_back_board(nw: NodeWrangler): 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}) @@ -144,7 +203,11 @@ def nodegroup_back_board(nw: NodeWrangler): 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}, @@ -152,16 +215,33 @@ def nodegroup_division_board(nw: NodeWrangler, tag_support=False): 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"]}) @@ -176,6 +256,7 @@ def nodegroup_division_boards(nw: NodeWrangler): 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}) @@ -192,12 +273,20 @@ def nodegroup_division_boards(nw: NodeWrangler): 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}) @@ -249,6 +338,12 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -257,14 +352,41 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -275,9 +397,23 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -291,10 +427,26 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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]}) @@ -303,6 +455,8 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -335,10 +489,14 @@ def get_asset_params(self, i=0): 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) @@ -350,6 +508,7 @@ def create_asset(self, i=0, **params): 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] From bbfa6bf4c771eee4b7c72d013198f265a5bf565b Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 503/727] Add 2 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/simple_bookcase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/simple_bookcase.py b/infinigen/assets/shelves/simple_bookcase.py index d88e22a4e..2a996a9de 100644 --- a/infinigen/assets/shelves/simple_bookcase.py +++ b/infinigen/assets/shelves/simple_bookcase.py @@ -5,12 +5,14 @@ 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 From 1272cee21a8cff02872fc7cd9a89495f63092056 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 504/727] Add 1 lines to infinigen/assets/shelves/simple_bookcase.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/simple_bookcase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/simple_bookcase.py b/infinigen/assets/shelves/simple_bookcase.py index 2a996a9de..6ea9796a9 100644 --- a/infinigen/assets/shelves/simple_bookcase.py +++ b/infinigen/assets/shelves/simple_bookcase.py @@ -503,6 +503,7 @@ def create_asset(self, i=0, **params): 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 From 56e2414ed2e8b241d3832c2d2946629b6be683e1 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 505/727] Add 612 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/large_shelf.py | 612 ++++++++++++++++++++++++ 1 file changed, 612 insertions(+) create mode 100644 infinigen/assets/shelves/large_shelf.py diff --git a/infinigen/assets/shelves/large_shelf.py b/infinigen/assets/shelves/large_shelf.py new file mode 100644 index 000000000..943387e91 --- /dev/null +++ b/infinigen/assets/shelves/large_shelf.py @@ -0,0 +1,612 @@ +# Copyright (c) Princeton University. +# This 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 + +import bpy +from infinigen.assets.shelves.utils import nodegroup_tagged_cube + 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 + + 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 From a8d1aca05375a2d008e6cb6deaef0c0d8284f8f8 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:27 -0700 Subject: [PATCH 506/727] Add 2 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/large_shelf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/large_shelf.py b/infinigen/assets/shelves/large_shelf.py index 943387e91..3604fff9c 100644 --- a/infinigen/assets/shelves/large_shelf.py +++ b/infinigen/assets/shelves/large_shelf.py @@ -10,9 +10,11 @@ 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, From bfc747d7c8e23e6498b2b38fbad80b9d7e7f3952 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 507/727] Add 1 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/large_shelf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/large_shelf.py b/infinigen/assets/shelves/large_shelf.py index 3604fff9c..feb191705 100644 --- a/infinigen/assets/shelves/large_shelf.py +++ b/infinigen/assets/shelves/large_shelf.py @@ -592,6 +592,7 @@ def create_asset(self, i=0, **params): if params.get('ret_params', False): return obj, obj_params + tagging.tag_system.relabel_obj(obj) return obj From 7d0f94b0b78da243f6ba47d9d65676207e883067 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 508/727] Add 1 lines to infinigen/assets/shelves/large_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/large_shelf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/large_shelf.py b/infinigen/assets/shelves/large_shelf.py index feb191705..2ba1950ae 100644 --- a/infinigen/assets/shelves/large_shelf.py +++ b/infinigen/assets/shelves/large_shelf.py @@ -592,6 +592,7 @@ def create_asset(self, i=0, **params): if params.get('ret_params', False): return obj, obj_params + tagging.tag_system.relabel_obj(obj) return obj From e98f5ba071acf90d6384ec663887c82b44284b02 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 509/727] Add 8 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 infinigen/assets/shelves/__init__.py diff --git a/infinigen/assets/shelves/__init__.py b/infinigen/assets/shelves/__init__.py new file mode 100644 index 000000000..8dbe8de5d --- /dev/null +++ b/infinigen/assets/shelves/__init__.py @@ -0,0 +1,8 @@ +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 3701bcf9108f91d0d4415d609f13354acd2308ca Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 510/727] Add 1 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/__init__.py b/infinigen/assets/shelves/__init__.py index 8dbe8de5d..8a7805e9f 100644 --- a/infinigen/assets/shelves/__init__.py +++ b/infinigen/assets/shelves/__init__.py @@ -1,3 +1,4 @@ +from .simple_desk import SimpleDeskFactory, SidetableDeskFactory from .simple_bookcase import SimpleBookcaseFactory from .cell_shelf import CellShelfFactory, TVStandFactory from .triangle_shelf import TriangleShelfFactory From 6d0b3104457c2058c1df375c04325b794cbf733b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 511/727] Add 1 lines to infinigen/assets/shelves/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/__init__.py b/infinigen/assets/shelves/__init__.py index 8a7805e9f..7fea19c8b 100644 --- a/infinigen/assets/shelves/__init__.py +++ b/infinigen/assets/shelves/__init__.py @@ -7,3 +7,4 @@ from .single_cabinet import SingleCabinetFactory from .kitchen_cabinet import KitchenCabinetFactory from .kitchen_space import KitchenSpaceFactory, KitchenIslandFactory +from .countertop import CountertopFactory From c0fed2318c0807ad07ab767e4689fb394e6b5ebd Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 512/727] Add 137 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/shelves/kitchen_space.py | 137 ++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 infinigen/assets/shelves/kitchen_space.py diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py new file mode 100644 index 000000000..445375a70 --- /dev/null +++ b/infinigen/assets/shelves/kitchen_space.py @@ -0,0 +1,137 @@ +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.util.color import color_category +from infinigen.core import surface + +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 + +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'}) + + + 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): + super(KitchenSpaceFactory, self).__init__(factory_seed, coarse=coarse) + + + 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: + + return box + + def create_asset(self, **params): + x, y, z = self.dimensions + 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) + + surface.add_geomod(cabinet_bottom, geometry_nodes_add_cabinet_top, apply=True) + + + + + + + + butil.apply_transform(kitchen_space) + + return kitchen_space From 51127c47dd4ebd4074821d5ec2efc5bf72e0a3b6 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 513/727] Add 68 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/kitchen_space.py | 76 +++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 445375a70..1141ca84e 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -1,6 +1,9 @@ +# 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 @@ -102,8 +105,24 @@ def geometry_node_to_bbox(nw: NodeWrangler): 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: + + self.island = island + if self.island: + dimensions.x *= uniform(1.5, 2) + + self.dimensions = dimensions self.params = self.sample_parameters(dimensions) @@ -113,25 +132,74 @@ def sample_parameters(self, dimensions): self.cabinet_top_height = uniform(0.8, 1.0) def create_placeholder(self, **kwargs) -> bpy.types.Object: + x, y, z = self.dimensions + 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 + + + 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): - - butil.apply_transform(kitchen_space) - - return kitchen_space + super(KitchenIslandFactory, self).__init__( + factory_seed=factory_seed, + island=True, + ) \ No newline at end of file From 9e8fd22a8837cb62cebc3dd400baef572e6129ba Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 514/727] Add 6 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/shelves/kitchen_space.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 1141ca84e..793f9a906 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -23,6 +23,7 @@ from infinigen.core.util import blender as butil from infinigen.assets.tables.table_top import nodegroup_generate_table_top +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 @@ -117,6 +118,11 @@ def __init__( 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: From 38696c640d78a552f897ae3d330409d3c523fc18 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 515/727] Add 5 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/shelves/kitchen_space.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 793f9a906..144142014 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -1,3 +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: Yiming Zuo, Stamatis Alexandropoulos import bpy @@ -190,6 +193,8 @@ def create_asset(self, **params): 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]) From 8b6f62e970673a62834d0571d11c7e330479e722 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 516/727] Add 2 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/kitchen_space.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 144142014..02e471f0d 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -13,6 +13,7 @@ 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 @@ -37,6 +38,7 @@ def nodegroup_tag_cube(nw: NodeWrangler): 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}) From df3f6db0a62d92294b2c70750ed13293da6dc22b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 517/727] Add 1 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/shelves/kitchen_space.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 02e471f0d..665794691 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -27,6 +27,7 @@ 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): From aa4be2069aa73e9b139c1de9a7d8e488dca4dcc1 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 518/727] Add 1 lines to infinigen/assets/shelves/kitchen_space.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/shelves/kitchen_space.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/kitchen_space.py b/infinigen/assets/shelves/kitchen_space.py index 665794691..66c65efdb 100644 --- a/infinigen/assets/shelves/kitchen_space.py +++ b/infinigen/assets/shelves/kitchen_space.py @@ -145,6 +145,7 @@ def sample_parameters(self, dimensions): 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: From f3aa5d46804f3079077b0a12fecdcd68484a45b2 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 519/727] Add 737 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/cabinet.py | 737 ++++++++++++++++++++++++++++ 1 file changed, 737 insertions(+) create mode 100644 infinigen/assets/shelves/cabinet.py diff --git a/infinigen/assets/shelves/cabinet.py b/infinigen/assets/shelves/cabinet.py new file mode 100644 index 000000000..0f34d72fa --- /dev/null +++ b/infinigen/assets/shelves/cabinet.py @@ -0,0 +1,737 @@ +# Copyright (c) Princeton University. +# This 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 + +import bpy +from infinigen.assets.shelves.utils import nodegroup_tagged_cube, blender_rotate +from infinigen.assets.shelves.large_shelf import LargeShelfBaseFactory, LargeShelfFactory, LargeShelfIkeaFactory + + +@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)}) + + + + 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]}) + + + 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'}) + + + + 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 + + + add = nw.new_node(Nodes.Math, + + add_1 = nw.new_node(Nodes.Math, input_kwargs={0: add, 1: group_input.outputs["length"]}) + + cylinder = nw.new_node('GeometryNodeMeshCylinder', + + + 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'}) + + + + 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 + + + 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}) + + 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, + + 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, + + 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, + + 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 + + + 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, + + 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 + + + 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'}) + + + 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}) + + + 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}) + + + index = nw.new_node(Nodes.Index) + + less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_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'}) + + + combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_5, 'Y': add_4}) + + + + 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}) + + + 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)}) + + + 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 + + + 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'}) + + + 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}) + + + 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}) + + + index = nw.new_node(Nodes.Index) + + less_than = nw.new_node(Nodes.Math, input_kwargs={0: index, 1: 1.0100}, attrs={'operation': 'LESS_THAN'}) + + + op_and = nw.new_node(Nodes.BooleanMath, input_kwargs={0: less_than, 1: greater_than}) + + + combine_xyz_9 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_2, 'Y': add_4}) + + + + 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}) + + + 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}) + + + 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 + + + attrs={'operation': 'MULTIPLY_ADD'}) + + + + + 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}) + + + 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}) + + + combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply_add}) + + + + join_geometry_1 = nw.new_node(Nodes.JoinGeometry, + input_kwargs={'Geometry': [transform_3, transform_2, transform_1, transform]}) + + 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'] + + + door_width = nw.new_node(Nodes.Value, label='door_width') + door_width.outputs[0].default_value = kwargs['door_width'] + + + 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']: + else: + + 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']: + frame.append(transform_5) + + join_geometry_1 = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': frame}) + + set_material_2 = nw.new_node(Nodes.SetMaterial, + + 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'}) + + + set_material_3 = nw.new_node(Nodes.SetMaterial, + + 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, + attach_gadgets.append(set_material_1) + + geos = [set_material_2, set_material_3, mid_board.outputs["Geometry"]] + attach_gadgets + + + combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': multiply}) + + + realize_instances_1 = nw.new_node(Nodes.RealizeInstances, input_kwargs={'Geometry': transform}) + + triangulate = nw.new_node('GeometryNodeTriangulate', input_kwargs={'Mesh': realize_instances_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}) + + +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 = [] + doors.append(transform_r) + if len(kwargs['door_hinge_pos']) > 1: + 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_2 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': transform_1, 'Rotation': (0.0000, 0.0000, -1.5708)}) + + + attaches.append(transform_3) + + join_geometry_a = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': attaches}) + + + + 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): + materials = [] + if not isinstance(params['board_material'], list): + params['board_material'] = [params['board_material']] + for mat in params['board_material']: + params['board_material'] = materials + return params + + def create_asset(self, i=0, **params): + 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_shelf_params(self, i=0): + params = self.shelf_params.copy() + if params.get('shelf_cell_width', None) is None: + 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] - + 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., + params['door_open_angle'] = 0 + elif self.door_params['num_door'] == 2: + params['door_hinge_pos'] = [(self.shelf_params['shelf_depth'] / 2. + 0.008, -shelf_width / 2., + params['door_open_angle'] = 0 + else: + raise NotImplementedError + + return params + + def get_cabinet_components(self, i): + # update material params + self.sample_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): + 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['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() + From 5bc55a43bf31fc230fa7c580231765d6846bad1f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 520/727] Add 264 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/cabinet.py | 264 ++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) diff --git a/infinigen/assets/shelves/cabinet.py b/infinigen/assets/shelves/cabinet.py index 0f34d72fa..4f0ca39a5 100644 --- a/infinigen/assets/shelves/cabinet.py +++ b/infinigen/assets/shelves/cabinet.py @@ -14,6 +14,7 @@ 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.core.util.math import FixedSeed @node_utils.to_nodegroup('nodegroup_node_group', singleton=False, type='GeometryNodeTree') @@ -22,7 +23,14 @@ def nodegroup_node_group(nw: NodeWrangler): 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)}) @@ -31,13 +39,19 @@ def nodegroup_node_group(nw: NodeWrangler): 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}) @@ -47,13 +61,25 @@ def nodegroup_node_group(nw: NodeWrangler): 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'}) @@ -61,7 +87,17 @@ def nodegroup_knob_handle(nw: NodeWrangler): 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}) @@ -71,11 +107,15 @@ def nodegroup_knob_handle(nw: NodeWrangler): 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'}) @@ -95,10 +135,13 @@ def nodegroup_mid_board(nw: NodeWrangler, **kwargs): 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'}) @@ -107,6 +150,7 @@ def nodegroup_mid_board(nw: NodeWrangler, **kwargs): 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]}) @@ -121,6 +165,8 @@ def nodegroup_mid_board(nw: NodeWrangler, **kwargs): 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}) @@ -147,6 +193,7 @@ def nodegroup_mid_board_001(nw: NodeWrangler, **kwargs): 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}) @@ -161,6 +208,9 @@ def nodegroup_mid_board_001(nw: NodeWrangler, **kwargs): 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}) @@ -188,11 +238,18 @@ def nodegroup_double_rampled_edge(nw: NodeWrangler): 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}) @@ -200,20 +257,39 @@ def nodegroup_double_rampled_edge(nw: NodeWrangler): 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}) @@ -243,6 +319,8 @@ def nodegroup_double_rampled_edge(nw: NodeWrangler): 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}) @@ -251,6 +329,11 @@ def nodegroup_double_rampled_edge(nw: NodeWrangler): 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]}) @@ -270,6 +353,9 @@ def nodegroup_double_rampled_edge(nw: NodeWrangler): 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}) @@ -295,11 +381,18 @@ def nodegroup_ramped_edge(nw: NodeWrangler): 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}) @@ -307,18 +400,37 @@ def nodegroup_ramped_edge(nw: NodeWrangler): 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}) @@ -350,6 +462,8 @@ def nodegroup_ramped_edge(nw: NodeWrangler): 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]}) @@ -364,6 +478,8 @@ def nodegroup_ramped_edge(nw: NodeWrangler): 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}) @@ -373,11 +489,24 @@ def nodegroup_ramped_edge(nw: NodeWrangler): 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}) @@ -385,19 +514,36 @@ def nodegroup_panel_edge_frame(nw: NodeWrangler): 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}) @@ -419,10 +565,31 @@ def geometry_door_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -430,18 +597,43 @@ def geometry_door_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -451,8 +643,18 @@ def geometry_door_nodes(nw: NodeWrangler, **kwargs): 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 = [] @@ -464,18 +666,28 @@ def geometry_door_nodes(nw: NodeWrangler, **kwargs): 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)}) @@ -491,8 +703,18 @@ def geometry_cabinet_nodes(nw: NodeWrangler, **kwargs): 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 = [] @@ -507,16 +729,26 @@ def geometry_cabinet_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -568,14 +800,18 @@ def get_asset_params(self, i=0): 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) @@ -629,10 +865,18 @@ 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 = [] @@ -658,6 +902,10 @@ def get_door_params(self, i=0): 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']) @@ -672,10 +920,22 @@ def get_cabinet_params(self, i=0): 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 @@ -684,6 +944,7 @@ def get_cabinet_params(self, i=0): 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) @@ -707,6 +968,8 @@ def get_cabinet_components(self, i): 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) @@ -727,6 +990,7 @@ def create_asset(self, i=0, **params): 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 From 39f752ea2500018190d8118a6a6ba80411668a2d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 521/727] Add 2 lines to infinigen/assets/shelves/cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/cabinet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/cabinet.py b/infinigen/assets/shelves/cabinet.py index 4f0ca39a5..517816530 100644 --- a/infinigen/assets/shelves/cabinet.py +++ b/infinigen/assets/shelves/cabinet.py @@ -10,10 +10,12 @@ 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 From a8892799af81729b09b0390c6f0bd4d40f030e1a Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 522/727] Add 52 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/utils.py | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 infinigen/assets/shelves/utils.py diff --git a/infinigen/assets/shelves/utils.py b/infinigen/assets/shelves/utils.py new file mode 100644 index 000000000..e79b4cc5b --- /dev/null +++ b/infinigen/assets/shelves/utils.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: 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 + + +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 + + + cube = nw.new_node(Nodes.MeshCube, input_kwargs={'Size': group_input.outputs["Size"]}) + + + + + + + + +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() From ba77992b08994124ac5d1aba2297f36405b8950c Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 523/727] Add 7 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/shelves/utils.py b/infinigen/assets/shelves/utils.py index e79b4cc5b..4ee552a45 100644 --- a/infinigen/assets/shelves/utils.py +++ b/infinigen/assets/shelves/utils.py @@ -9,6 +9,8 @@ from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler, geometry_node_group_empty_new from infinigen.core.nodes import node_utils +from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo + def get_nodegroup_assets(func, params): bpy.ops.mesh.primitive_plane_add( @@ -27,13 +29,18 @@ def get_nodegroup_assets(func, params): 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'}) + #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}) From 965de98c3b91f11b044a7f4a0c8223129142a09d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 524/727] Add 2 lines to infinigen/assets/shelves/utils.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/utils.py b/infinigen/assets/shelves/utils.py index 4ee552a45..be55e31e6 100644 --- a/infinigen/assets/shelves/utils.py +++ b/infinigen/assets/shelves/utils.py @@ -8,6 +8,7 @@ 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 @@ -37,6 +38,7 @@ def nodegroup_tagged_cube(nw: NodeWrangler): 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}) From e149bd996e05244ef18bdd2bd06670277cb32020 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 525/727] Add 154 lines to infinigen/assets/shelves/countertop.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/countertop.py | 154 +++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 infinigen/assets/shelves/countertop.py 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) From 3fe9de84875a1731c0bda4a48cd76d42ad8c2ffa Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 526/727] Add 211 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/simple_desk.py | 211 ++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 infinigen/assets/shelves/simple_desk.py diff --git a/infinigen/assets/shelves/simple_desk.py b/infinigen/assets/shelves/simple_desk.py new file mode 100644 index 000000000..5a4420109 --- /dev/null +++ b/infinigen/assets/shelves/simple_desk.py @@ -0,0 +1,211 @@ +# Copyright (c) Princeton University. +# This 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 + +import bpy +from infinigen.assets.shelves.utils import nodegroup_tagged_cube + 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 + + + subtract = nw.new_node(Nodes.Math, + input_kwargs={0: group_input.outputs["height"], 1: group_input.outputs["thickness"]}, + attrs={'operation': 'SUBTRACT'}) + + + + 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'}) + + + transform = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_2}) + + + + transform_2 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_3}) + + + + transform_3 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': cylinder.outputs["Mesh"], 'Translation': combine_xyz_4}) + + + 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 + + + add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["thickness"], 1: 0.0000}) + + + if tag_support: + cube = nw.new_node(nodegroup_tagged_cube().name, input_kwargs={'Size': combine_xyz}) + + else: + + multiply = nw.new_node(Nodes.Math, input_kwargs={0: add}, attrs={'operation': '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'] + + + set_material = nw.new_node(Nodes.SetMaterial, + + 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'] + + + set_material_1 = nw.new_node(Nodes.SetMaterial, + + 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}) + + + 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): + 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) + + return obj + + +class SimpleDeskFactory(SimpleDeskBaseFactory): + def sample_params(self): + params = dict() + params['depth'] = params['Dimensions'][0] + params['width'] = params['Dimensions'][1] + params['height'] = params['Dimensions'][2] + return params + + From 991ab2529577da5a1b3fe5805c75b8f703cd7c96 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 527/727] Add 43 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/shelves/simple_desk.py | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/infinigen/assets/shelves/simple_desk.py b/infinigen/assets/shelves/simple_desk.py index 5a4420109..72a667bc5 100644 --- a/infinigen/assets/shelves/simple_desk.py +++ b/infinigen/assets/shelves/simple_desk.py @@ -23,12 +23,22 @@ 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}) @@ -41,20 +51,26 @@ def nodegroup_table_legs(nw: NodeWrangler): 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}) @@ -72,17 +88,29 @@ def nodegroup_table_legs(nw: NodeWrangler): 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}) @@ -108,8 +136,13 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -117,8 +150,13 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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]}) @@ -126,6 +164,7 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -192,6 +231,8 @@ def get_material_func(self, params, randomness=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) @@ -203,6 +244,8 @@ def create_asset(self, i=0, **params): class SimpleDeskFactory(SimpleDeskBaseFactory): def sample_params(self): params = dict() + params['Dimensions'] = (uniform(0.5, 0.75), + uniform(0.6, 0.8)) params['depth'] = params['Dimensions'][0] params['width'] = params['Dimensions'][1] params['height'] = params['Dimensions'][2] From 0d7e35eadb0c1e53ed06e24dd778ef969ac3b51d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 528/727] Add 11 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/simple_desk.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/infinigen/assets/shelves/simple_desk.py b/infinigen/assets/shelves/simple_desk.py index 72a667bc5..5a3ee7724 100644 --- a/infinigen/assets/shelves/simple_desk.py +++ b/infinigen/assets/shelves/simple_desk.py @@ -237,6 +237,7 @@ def create_asset(self, i=0, **params): 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 @@ -245,10 +246,20 @@ 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 From 62b3fcfffb46169d9f504bcd14dfa66095204ce4 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 529/727] Add 2 lines to infinigen/assets/shelves/simple_desk.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/simple_desk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/simple_desk.py b/infinigen/assets/shelves/simple_desk.py index 5a3ee7724..440ae9fe5 100644 --- a/infinigen/assets/shelves/simple_desk.py +++ b/infinigen/assets/shelves/simple_desk.py @@ -10,9 +10,11 @@ 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, From a727f1940a31e291466e4376e8297f4de1d89aef Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 530/727] Add 526 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/triangle_shelf.py | 526 +++++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 infinigen/assets/shelves/triangle_shelf.py diff --git a/infinigen/assets/shelves/triangle_shelf.py b/infinigen/assets/shelves/triangle_shelf.py new file mode 100644 index 000000000..6c9988768 --- /dev/null +++ b/infinigen/assets/shelves/triangle_shelf.py @@ -0,0 +1,526 @@ +# Copyright (c) Princeton University. +# This 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 + +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 + + + value = nw.new_node(Nodes.Value) + value.outputs[0].default_value = 0.7071 + + + 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)}) + + + + transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': transform_2, 'Scale': combine_xyz}) + + + + 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 + + + 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) + + + + position_1 = nw.new_node(Nodes.InputPosition) + + separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1}) + + + 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}) + + + +@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 + + + 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}) + + + spline_parameter_1 = nw.new_node(Nodes.SplineParameter) + + + + + position_1 = nw.new_node(Nodes.InputPosition) + + separate_xyz_2 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position_1}) + + + 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}) + + + + + + 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)}) + + + 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}) + + + curve_line_3 = nw.new_node(Nodes.CurveLine, input_kwargs={'Start': combine_xyz_3, 'End': combine_xyz_6}) + + + 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}) + + + 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}) + + + + + 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}) + + + + curve_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={'Profile Curve': transform_6}) + + attrs={'operation': 'MULTIPLY'}) + + combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply}) + + + 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)}) + + + + 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}) + + attrs={'operation': 'MULTIPLY'}) + + combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Z': multiply}) + + + 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, + + add = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["x5"], 1: 0.0000}) + + + 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"]}) + + + 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 + + + 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"]}) + + + combine_xyz_4 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': group_input.outputs["Mid_z"]}) + + + combine_xyz_5 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'X': add, 'Z': group_input.outputs["Top_z"]}) + + + 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}) + + + + 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}) + + + 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}) + + + 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}) + + + 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 + + + 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}) + + + add_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["leg_curve_ratio"], 1: 0.0000}) + + + 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}) + + + 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}) + + + combine_xyz_2 = nw.new_node(Nodes.CombineXYZ, input_kwargs={'Y': add_3}) + + + 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'] + + + set_material = nw.new_node(Nodes.SetMaterial, + + 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'] + + + join_geometry2 = nw.new_node(Nodes.JoinGeometry, + + set_material_1 = nw.new_node(Nodes.SetMaterial, + + side_board_height = nw.new_node(Nodes.Value, label='side_board_height') + side_board_height.outputs[0].default_value = kwargs['side_board_height'] + + + set_material_3 = nw.new_node(Nodes.SetMaterial, + + + 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, + + 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): + return params + + def create_asset(self, i=0, **params): + obj = bpy.context.active_object + + obj_params = self.get_asset_params(i) + + return obj + + +class TriangleShelfFactory(TriangleShelfBaseFactory): + def sample_params(self): + params = dict() + params['leg_length'] = params['Dimensions'][2] + params['board_width'] = params['Dimensions'][0] + return params + From cbad3eff77e68cb5385d02874ea5462265d18e83 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 531/727] Add 340 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/triangle_shelf.py | 340 +++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/infinigen/assets/shelves/triangle_shelf.py b/infinigen/assets/shelves/triangle_shelf.py index 6c9988768..1945b445b 100644 --- a/infinigen/assets/shelves/triangle_shelf.py +++ b/infinigen/assets/shelves/triangle_shelf.py @@ -4,6 +4,7 @@ # 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 @@ -20,11 +21,19 @@ 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}) @@ -35,11 +44,30 @@ def nodegroup_table_profile(nw: NodeWrangler): 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}) @@ -49,6 +77,8 @@ def nodegroup_table_profile(nw: NodeWrangler): 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'}) @@ -64,12 +94,25 @@ def nodegroup_curve_to_board(nw: NodeWrangler): 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"]}) @@ -90,12 +133,40 @@ def nodegroup_curve_to_board(nw: NodeWrangler): 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'}) @@ -106,16 +177,40 @@ def nodegroup_leg_straight(nw: NodeWrangler): 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"]}) @@ -136,9 +231,27 @@ def nodegroup_leg_straight(nw: NodeWrangler): 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}, @@ -152,6 +265,9 @@ def nodegroup_curve_board(nw: NodeWrangler): 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"]}) @@ -161,14 +277,25 @@ def nodegroup_curve_board(nw: NodeWrangler): 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}) @@ -176,8 +303,19 @@ def nodegroup_curve_board(nw: NodeWrangler): 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)}) @@ -189,14 +327,28 @@ def nodegroup_curve_board(nw: NodeWrangler): 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]}) @@ -219,7 +371,27 @@ def nodegroup_side_leg(nw: NodeWrangler): 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)}) @@ -231,10 +403,31 @@ def nodegroup_side_leg(nw: NodeWrangler): 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"]]}) @@ -255,9 +448,18 @@ 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}) @@ -266,6 +468,8 @@ def nodegroup_side_boards(nw: NodeWrangler): 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'}) @@ -291,17 +495,43 @@ def nodegroup_side_boards(nw: NodeWrangler): 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]}) @@ -316,7 +546,15 @@ def nodegroup_screw_head(nw: NodeWrangler): 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'}) @@ -337,11 +575,15 @@ def nodegroup_screw_head(nw: NodeWrangler): 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'}) @@ -352,6 +594,8 @@ def nodegroup_screw_head(nw: NodeWrangler): 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]}) @@ -364,19 +608,40 @@ def nodegroup_screw_head(nw: NodeWrangler): 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}) @@ -389,9 +654,13 @@ def nodegroup_shelf_legs(nw: NodeWrangler): 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}) @@ -423,8 +692,17 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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'] @@ -441,17 +719,73 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -460,6 +794,7 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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}) @@ -507,9 +842,13 @@ def get_asset_params(self, i=0): 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) @@ -520,6 +859,7 @@ def create_asset(self, i=0, **params): 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 From 2a2af220d1ea9d04b930a78267056aff372a54d3 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:28 -0700 Subject: [PATCH 532/727] Add 2 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/triangle_shelf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/triangle_shelf.py b/infinigen/assets/shelves/triangle_shelf.py index 1945b445b..c8a97ac26 100644 --- a/infinigen/assets/shelves/triangle_shelf.py +++ b/infinigen/assets/shelves/triangle_shelf.py @@ -5,12 +5,14 @@ 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 897c7ab545b0d1f477d69bdc8d4d22e7a7981dbd Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 533/727] Add 2 lines to infinigen/assets/shelves/triangle_shelf.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/triangle_shelf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/triangle_shelf.py b/infinigen/assets/shelves/triangle_shelf.py index c8a97ac26..3eb60ef98 100644 --- a/infinigen/assets/shelves/triangle_shelf.py +++ b/infinigen/assets/shelves/triangle_shelf.py @@ -854,6 +854,8 @@ def create_asset(self, i=0, **params): 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 From 94f8683312c79138ae50e30d08e52cc718f45651 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 534/727] Add 215 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/single_cabinet.py | 215 +++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 infinigen/assets/shelves/single_cabinet.py diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py new file mode 100644 index 000000000..0e622ee44 --- /dev/null +++ b/infinigen/assets/shelves/single_cabinet.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: 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 + +import bpy +from infinigen.assets.shelves.utils import nodegroup_tagged_cube, blender_rotate +from infinigen.assets.shelves.doors import CabinetDoorBaseFactory + + +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) + + def sample_params(self): + # Update fac params + pass + + + 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 + + # 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) + '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]) + + 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 From dbd22db1fd5834cb0b5c9236af9047f12ff74acd Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 535/727] Add 9 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/single_cabinet.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py index 0e622ee44..0439bb1b3 100644 --- a/infinigen/assets/shelves/single_cabinet.py +++ b/infinigen/assets/shelves/single_cabinet.py @@ -13,8 +13,10 @@ 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 def geometry_cabinet_nodes(nw: NodeWrangler, **kwargs): # Code generated using version 2.6.4 of the node_transpiler @@ -80,6 +82,12 @@ 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() @@ -155,6 +163,7 @@ def get_cabinet_params(self, i=0): 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) From a8801ada35446a66a6fdc6cd5e8ee5e7046a6cde Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 536/727] Add 7 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/shelves/single_cabinet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py index 0439bb1b3..b34ef422d 100644 --- a/infinigen/assets/shelves/single_cabinet.py +++ b/infinigen/assets/shelves/single_cabinet.py @@ -17,6 +17,7 @@ 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 @@ -77,6 +78,8 @@ def __init__(self, factory_seed, params={}, coarse=False): 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 @@ -222,3 +225,7 @@ def sample_params(self): 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 From 5b64e3dbeb881c38f52cf1fcbc3c9de009f729fa Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 537/727] Add 3 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/single_cabinet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py index b34ef422d..0d65cda5f 100644 --- a/infinigen/assets/shelves/single_cabinet.py +++ b/infinigen/assets/shelves/single_cabinet.py @@ -198,6 +198,7 @@ def create_asset(self, i=0, **params): # 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'], @@ -207,6 +208,7 @@ def create_asset(self, i=0, **params): butil.delete([left_door, right_door]) obj = butil.join_objects([shelf, obj]) + tagging.tag_system.relabel_obj(obj) return obj @@ -229,3 +231,4 @@ def sample_params(self): 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) From f37e73c41832f394fa9a631a7c6e2e2f0832a6fc Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 538/727] Add 1 lines to infinigen/assets/shelves/single_cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/single_cabinet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/single_cabinet.py b/infinigen/assets/shelves/single_cabinet.py index 0d65cda5f..14dc77257 100644 --- a/infinigen/assets/shelves/single_cabinet.py +++ b/infinigen/assets/shelves/single_cabinet.py @@ -10,6 +10,7 @@ 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 2f5a06be2ca4989bf805e5bbfa694fc650a5a5df Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 539/727] Add 280 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/kitchen_cabinet.py | 280 ++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 infinigen/assets/shelves/kitchen_cabinet.py diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py new file mode 100644 index 000000000..17d14c7e2 --- /dev/null +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -0,0 +1,280 @@ +# Copyright (c) Princeton University. +# This 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 + +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 + shader_shelves_white, shader_shelves_white_sampler, + shader_shelves_black_wood, shader_shelves_black_wood_sampler, + + +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) + + 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) + + def sample_params(self): + pass + + + 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 = [] + 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 + + # update 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 + 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): + 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) + return obj + + +class KitchenCabinetFactory(KitchenCabinetBaseFactory): + def sample_params(self): + params = dict() + uniform(0.25, 0.35), + uniform(1.0, 4.0), + + 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 + + intervals = np.random.uniform(0.55, 1.0, size=(n_cells,)) + intervals = intervals / intervals.sum() * params['Dimensions'][1] + self.cabinet_widths = intervals.tolist() + From 42d9faa1e4aba5d2ae6487f6f88705b70a5dad3b Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 540/727] Add 16 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/shelves/kitchen_cabinet.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py index 17d14c7e2..44fcc7819 100644 --- a/infinigen/assets/shelves/kitchen_cabinet.py +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -10,6 +10,8 @@ import numpy as np from infinigen.core.util import blender as butil +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 @@ -71,6 +73,7 @@ def __init__(self, factory_seed, params={}, coarse=False): self.frame_fac = LargeShelfBaseFactory(factory_seed) self.door_fac = CabinetDoorBaseFactory(factory_seed) self.drawer_fac = CabinetDrawerBaseFactory(factory_seed) + self.drawer_only = False def sample_params(self): pass @@ -180,6 +183,7 @@ def get_cabinet_params(self, i=0): accum_w += thickness + w / 2. + 0.0005 return x_translations + def create_cabinet_components(self, i, drawer_only=False): # update material params components = [] @@ -192,6 +196,7 @@ def get_cabinet_params(self, i=0): 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]) @@ -226,6 +231,7 @@ def get_cabinet_params(self, i=0): 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 = [] @@ -262,10 +268,20 @@ def create_asset(self, i=0, **params): 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)) + else: + dimensions = self.dimensions + params['Dimensions'] = dimensions params['bottom_board_height'] = 0.06 params['shelf_depth'] = params['Dimensions'][0] - 0.01 From 3463d08b13e3e16c87e248776a528b361ca59f06 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 541/727] Add 13 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/kitchen_cabinet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py index 44fcc7819..c8ac8f46a 100644 --- a/infinigen/assets/shelves/kitchen_cabinet.py +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -3,6 +3,7 @@ # 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 @@ -78,6 +79,14 @@ def __init__(self, factory_seed, params={}, coarse=False): 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): @@ -185,6 +194,7 @@ def get_cabinet_params(self, i=0): 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): @@ -264,6 +274,7 @@ def create_asset(self, i=0, **params): #butil.delete(c[:1]) obj = butil.join_objects(join_objs) + return obj @@ -272,6 +283,7 @@ def __init__(self, factory_seed, params={}, coarse=False, dimensions=None, drawe 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: @@ -290,6 +302,7 @@ def sample_params(self): 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() From 82cf48cbf9832590e5e8431f15a7788ce006e790 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 542/727] Add 9 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/kitchen_cabinet.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py index c8ac8f46a..5ffec69c4 100644 --- a/infinigen/assets/shelves/kitchen_cabinet.py +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -2,6 +2,7 @@ # This 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 @@ -20,6 +21,8 @@ from infinigen.assets.shelves.drawers import CabinetDrawerBaseFactory shader_shelves_white, shader_shelves_white_sampler, shader_shelves_black_wood, shader_shelves_black_wood_sampler, + shader_shelves_wood, shader_shelves_wood_sampler +) def geometry_nodes(nw: NodeWrangler, **kwargs): @@ -61,6 +64,10 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): '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}) @@ -186,6 +193,7 @@ def get_attach_params(self, attach_type, i=0): 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) @@ -274,6 +282,7 @@ def create_asset(self, i=0, **params): #butil.delete(c[:1]) obj = butil.join_objects(join_objs) + tagging.tag_system.relabel_obj(obj) return obj From cdaaaa8b7aeee8a6e134f2ce80c35288002cd777 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 543/727] Add 7 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/shelves/kitchen_cabinet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py index 5ffec69c4..3ce71bf26 100644 --- a/infinigen/assets/shelves/kitchen_cabinet.py +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -23,6 +23,7 @@ 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): @@ -82,6 +83,8 @@ def __init__(self, factory_seed, params={}, coarse=False): 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 @@ -300,6 +303,7 @@ def sample_params(self): 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 @@ -316,3 +320,6 @@ def sample_params(self): 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) From a8148ea6521708c9815230c7c294919458503a7e Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 544/727] Add 3 lines to infinigen/assets/shelves/kitchen_cabinet.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/kitchen_cabinet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/shelves/kitchen_cabinet.py b/infinigen/assets/shelves/kitchen_cabinet.py index 3ce71bf26..6dea292a0 100644 --- a/infinigen/assets/shelves/kitchen_cabinet.py +++ b/infinigen/assets/shelves/kitchen_cabinet.py @@ -5,12 +5,14 @@ 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 @@ -19,6 +21,7 @@ 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 69a2156ce49559873bd44f3e29893ad309802fb2 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 545/727] Add 724 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/shelves/doors.py | 724 ++++++++++++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100644 infinigen/assets/shelves/doors.py diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py new file mode 100644 index 000000000..386c3fdce --- /dev/null +++ b/infinigen/assets/shelves/doors.py @@ -0,0 +1,724 @@ +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 +import bpy + + shader_shelves_white, shader_shelves_white_sampler, + shader_shelves_black_wood, shader_shelves_black_wood_sampler, + shader_shelves_wood, shader_shelves_wood_sampler, + +@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)}) + + 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)}) + + 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: + 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) + + if params.get('ret_params', False): + return obj, obj_params + + return obj + From 347968be858735e7b9a2d9f7b18149d05aba4f09 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 546/727] Add 6 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/shelves/doors.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py index 386c3fdce..9ae89c3d8 100644 --- a/infinigen/assets/shelves/doors.py +++ b/infinigen/assets/shelves/doors.py @@ -1,3 +1,4 @@ + from numpy.random import uniform, normal, randint from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler from infinigen.core.nodes import node_utils @@ -500,6 +501,8 @@ def nodegroup_panel_edge_frame(nw: NodeWrangler): 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]}) @@ -610,6 +613,9 @@ def geometry_door_nodes(nw: NodeWrangler, **kwargs): 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)}) From 11888deef7bf8eb3a45489e4f3a8619a0a2be8ef Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 547/727] Add 6 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/shelves/doors.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py index 9ae89c3d8..1d82c32c0 100644 --- a/infinigen/assets/shelves/doors.py +++ b/infinigen/assets/shelves/doors.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: Lingjie Mei from numpy.random import uniform, normal, randint from infinigen.core.nodes.node_wrangler import Nodes, NodeWrangler @@ -11,6 +15,7 @@ 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): @@ -709,6 +714,7 @@ def get_material_func(self, params, randomness=True): 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) From a75c1d1ac9494cf7c0e4bc06771efa00174f80d0 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 548/727] Add 2 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/shelves/doors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py index 1d82c32c0..84ccda16e 100644 --- a/infinigen/assets/shelves/doors.py +++ b/infinigen/assets/shelves/doors.py @@ -10,8 +10,10 @@ 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, From e603c56b37178c276bd8eb170341f1a4e9422d20 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 549/727] Add 1 lines to infinigen/assets/shelves/doors.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/shelves/doors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/shelves/doors.py b/infinigen/assets/shelves/doors.py index 84ccda16e..78dc111c6 100644 --- a/infinigen/assets/shelves/doors.py +++ b/infinigen/assets/shelves/doors.py @@ -730,6 +730,7 @@ def create_asset(self, i=0, **params): 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 From df061ad8a57643f1050771311efbfa70e137041c Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 550/727] Add 434 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- .../assets/appliances/beverage_fridge.py | 434 ++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 infinigen/assets/appliances/beverage_fridge.py diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py new file mode 100644 index 000000000..68714968a --- /dev/null +++ b/infinigen/assets/appliances/beverage_fridge.py @@ -0,0 +1,434 @@ +# Copyright (c) Princeton University. +# This 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 import surface +from infinigen.core.util import blender as butil + +from infinigen.core.util.math import FixedSeed +from infinigen.core.placement.factory import AssetFactory + +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) + + @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() + return obj + +@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)), + 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)), + ('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, + 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, + 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, + 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, + 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, + 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, + 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}) + + # 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), + 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, + 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, + set_material_3 = nw.new_node(Nodes.SetMaterial, + 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, + 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, + set_material_9 = nw.new_node(Nodes.SetMaterial, + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) + 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}) + 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, + 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, + 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]}) + group_output = nw.new_node(Nodes.GroupOutput, From b372ee12b2366841efc5e30629eda549d311b2df Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 551/727] Add 223 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/appliances/beverage_fridge.py | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py index 68714968a..cae18a0d7 100644 --- a/infinigen/assets/appliances/beverage_fridge.py +++ b/infinigen/assets/appliances/beverage_fridge.py @@ -8,6 +8,7 @@ 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 @@ -51,9 +52,16 @@ def sample_parameters(dimensions): "BrandName": brand_name, } return params + def create_asset(self, **params): obj = butil.spawn_cube() 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): @@ -64,44 +72,67 @@ def nodegroup_oven_rack(nw: NodeWrangler): ('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') @@ -113,14 +144,19 @@ def nodegroup_text(nw: NodeWrangler): ('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') @@ -131,33 +167,54 @@ def nodegroup_handle(nw: NodeWrangler): 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') @@ -170,39 +227,57 @@ def nodegroup_center(nw: NodeWrangler): ('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') @@ -212,19 +287,25 @@ def nodegroup_cube(nw: NodeWrangler): 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)), + 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') @@ -241,99 +322,167 @@ def nodegroup_hollow_cube(nw: NodeWrangler): ('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, + 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, + 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, + 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, + 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, + 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, + 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}) # Code generated using version 2.6.5 of the node_transpiler @@ -347,88 +496,162 @@ def nodegroup_hollow_cube(nw: NodeWrangler): ('NodeSocketFloatDistance', 'RackRadius', 0.0100), ('NodeSocketInt', 'RackDAmount', 5), ('NodeSocketInt', 'RackHAmount', 2), + 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, + + + 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, + set_material_3 = nw.new_node(Nodes.SetMaterial, + + 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, + 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, + set_material_9 = nw.new_node(Nodes.SetMaterial, + + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) + + 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}) + + 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, + + 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, + + + 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}) From 345e61a6aa00bdcb5266c534604543bf77c3eba5 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 552/727] Add 42 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/appliances/beverage_fridge.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py index cae18a0d7..d30bcdd62 100644 --- a/infinigen/assets/appliances/beverage_fridge.py +++ b/infinigen/assets/appliances/beverage_fridge.py @@ -17,6 +17,7 @@ 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.]): @@ -25,7 +26,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): 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) @@ -55,6 +83,7 @@ def sample_parameters(dimensions): def create_asset(self, **params): obj = butil.spawn_cube() + return obj def finalize_assets(self, assets): @@ -496,6 +525,11 @@ def nodegroup_hollow_cube(nw: NodeWrangler): ('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"]}) @@ -505,6 +539,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): set_material_1 = nw.new_node(Nodes.SetMaterial, + input_kwargs={'Geometry': hollowcube, 'Material': group_input.outputs["Surface"]}) + @@ -521,8 +557,10 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: 0.0500}, attrs={'operation': 'MULTIPLY'}) @@ -546,6 +584,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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}) @@ -563,6 +602,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): text = nw.new_node(nodegroup_text().name, set_material_9 = nw.new_node(Nodes.SetMaterial, + input_kwargs={'Geometry': text, 'Material': group_input.outputs["Handle"]}) geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) @@ -625,6 +665,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) add_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]}) @@ -644,6 +685,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) From b07eeaaad3a4aeca036d264f5f9d97fde51de25e Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 553/727] Add 35 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- .../assets/appliances/beverage_fridge.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py index d30bcdd62..0c0e66a85 100644 --- a/infinigen/assets/appliances/beverage_fridge.py +++ b/infinigen/assets/appliances/beverage_fridge.py @@ -12,6 +12,8 @@ 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 @@ -83,6 +85,12 @@ def sample_parameters(dimensions): 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 @@ -316,6 +324,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -344,6 +353,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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), @@ -365,6 +375,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -400,6 +411,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -423,6 +435,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -448,6 +461,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -471,6 +485,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -492,6 +507,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -514,6 +530,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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, @@ -541,8 +559,11 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) @@ -562,6 +583,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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'}) @@ -600,19 +622,29 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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}, @@ -667,6 +699,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) @@ -687,7 +720,9 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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') From 348fe2a7cc2d8504c46846407ba3f2e905ae6709 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 554/727] Add 1 lines to infinigen/assets/appliances/beverage_fridge.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/appliances/beverage_fridge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/appliances/beverage_fridge.py b/infinigen/assets/appliances/beverage_fridge.py index 0c0e66a85..9ab8b5278 100644 --- a/infinigen/assets/appliances/beverage_fridge.py +++ b/infinigen/assets/appliances/beverage_fridge.py @@ -22,6 +22,7 @@ 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) From de1091a8b60bcde3ec5f42b6d700f523ff8bd20a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 555/727] Add 644 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/appliances/dishwasher.py | 644 ++++++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 infinigen/assets/appliances/dishwasher.py diff --git a/infinigen/assets/appliances/dishwasher.py b/infinigen/assets/appliances/dishwasher.py new file mode 100644 index 000000000..0afe0194e --- /dev/null +++ b/infinigen/assets/appliances/dishwasher.py @@ -0,0 +1,644 @@ + +from infinigen.assets.materials import metal + + "RackAmount": rack_h_amount, + + butil.modify_mesh(obj, 'NODES', node_group=nodegroup_dishwasher_geometry(), ng_inputs=self.params, apply=True) + + def finalize_assets(self, assets): + if self.scratch: + self.scratch.apply(assets) + if self.edge_wear: + self.edge_wear.apply(assets) + + 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'}) + + 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'}) + + input_kwargs={0: duplicate_elements_2.outputs["Duplicate Index"], 1: divide}, + attrs={'operation': 'MULTIPLY'}) + + + set_position_2 = nw.new_node(Nodes.SetPosition, input_kwargs={ + 'Geometry': duplicate_elements_2.outputs["Geometry"], + 'Offset': combine_xyz_3 + }) + + + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', + input_kwargs={'Geometry': join_geometry_1}) + + 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'}) + + + + 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)}) + + 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'}) + + + set_position_1 = nw.new_node(Nodes.SetPosition, input_kwargs={ + 'Geometry': duplicate_elements_1.outputs["Geometry"], + 'Offset': combine_xyz_1 + }) + + + multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: 0.8000}, + attrs={'operation': 'MULTIPLY'}) + + + 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_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'}) + + + 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}) + + 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}) + + group_input = nw.new_node(Nodes.GroupInput, expose_input=[('NodeSocketFloat', 'width', 0.0000), + ('NodeSocketFloat', 'length', 0.0000), ('NodeSocketFloat', 'thickness', 0.0200)]) + + + 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'}) + + + 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'}) + + + 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'}) + + + 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"] + }) + + + 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'}) + + + + transform_1 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_2}) + + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': join_geometry}, + attrs={'is_active_output': True}) + + 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)]) + + + input_kwargs={0: group_input.outputs["Vector"], 1: bounding_box.outputs["Min"]}, + attrs={'operation': 'SUBTRACT'}) + + + input_kwargs={0: separate_xyz.outputs["X"], 1: group_input.outputs["MarginX"]}, + attrs={'operation': 'GREATER_THAN', 'use_clamp': True}) + + input_kwargs={0: bounding_box.outputs["Max"], 1: group_input.outputs["Vector"]}, + attrs={'operation': 'SUBTRACT'}) + + + 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}) + + + 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}) + + + + 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}) + + + + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'In': op_and_4, 'Out': op_not}, + attrs={'is_active_output': True}) + + + 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'}) + + 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}) + + + + 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 + }) + + + 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'}) + + + + scale = nw.new_node(Nodes.VectorMath, input_kwargs={0: group_input.outputs["Size"], 'Scale': 0.5000}, + attrs={'operation': 'SCALE'}) + + + add_1 = nw.new_node(Nodes.Math, + input_kwargs={0: separate_xyz_2.outputs["Y"], 1: separate_xyz_1.outputs["Y"]}) + + input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]}, + attrs={'operation': 'SUBTRACT'}) + + + transform_2 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_1, 'Translation': combine_xyz_5}) + + + 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"] + }) + + + 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'}) + + + transform_1 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_4, 'Translation': combine_xyz_3}) + + + 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"] + }) + + + 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"]}) + + + + transform = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute, 'Translation': combine_xyz_1}) + + + 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 + }) + + + 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"]}) + + input_kwargs={0: separate_xyz_2.outputs["Z"], 1: separate_xyz_1.outputs["Z"]}, + attrs={'operation': 'SUBTRACT'}) + + + transform_3 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_5, 'Translation': combine_xyz_7}) + + + 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"] + }) + + + 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_10 = nw.new_node(Nodes.Math, + input_kwargs={0: separate_xyz_1.outputs["Z"], 1: separate_xyz_2.outputs["Z"]}) + + + transform_4 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_2, 'Translation': combine_xyz_8}) + + + 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"] + }) + + + 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"]}) + + + transform_5 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': store_named_attribute_3, 'Translation': combine_xyz_11}) + + + 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}) + + + 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 + }) + + + + + 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"] + }) + + + + + center = nw.new_node(nodegroup_center().name, input_kwargs={ + 'Geometry': cube, + 'Vector': position, + 'MarginX': -1.0000, + 'MarginY': 0.1000, + 'MarginZ': 0.1500 + }) + + + + + 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'}) + + + 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'}) + + + transform_1 = nw.new_node(Nodes.Transform, input_kwargs={ + 'Geometry': handle, + 'Translation': combine_xyz_13, + 'Rotation': (0.0000, 1.5708, 0.0000) + }) + + + 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'}) + + + 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 + }) + + + + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', + input_kwargs={'Geometry': join_geometry_3}) + + + + rotate_instances = nw.new_node(Nodes.RotateInstances, input_kwargs={ + 'Instances': geometry_to_instance, + 'Rotation': combine_xyz_3, + 'Pivot Point': combine_xyz_4 + }) + + + 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={ + 'Radius': group_input.outputs["RackRadius"], + 'Amount': 4, + 'Height': 0.1000 + }) + + + 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'}) + + + 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'}) + + + + + 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 + }) + + + + add_4 = nw.new_node(Nodes.Math, + input_kwargs={0: group_input.outputs["Depth"], 1: group_input.outputs["DoorThickness"]}) + + + + + combine_xyz_6 = nw.new_node(Nodes.CombineXYZ, + input_kwargs={'X': reroute_10, 'Y': reroute_11, 'Z': reroute_8}) + + + + + + + + + + geometry = nw.new_node(Nodes.RealizeInstances, [join_geometry]) + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': geometry}) From 98eeb1bbe2b8252da0a2f0f779f3d25bc27ee42f Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 556/727] Add 191 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/appliances/dishwasher.py | 193 +++++++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/infinigen/assets/appliances/dishwasher.py b/infinigen/assets/appliances/dishwasher.py index 0afe0194e..1ca0aae6f 100644 --- a/infinigen/assets/appliances/dishwasher.py +++ b/infinigen/assets/appliances/dishwasher.py @@ -1,9 +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. -from infinigen.assets.materials import metal +# 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 import surface +from infinigen.core.util import blender as butil + +from infinigen.core.util.math import FixedSeed +from infinigen.core.placement.factory import AssetFactory + +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) + + @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(), ng_inputs=self.params, apply=True) + return obj def finalize_assets(self, assets): if self.scratch: @@ -11,6 +63,12 @@ def finalize_assets(self, 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)}) @@ -30,25 +88,30 @@ def finalize_assets(self, assets): 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'}) @@ -57,7 +120,9 @@ def finalize_assets(self, assets): 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"], @@ -67,6 +132,7 @@ def finalize_assets(self, assets): 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'}) @@ -78,16 +144,19 @@ def finalize_assets(self, assets): 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}) @@ -96,6 +165,7 @@ def finalize_assets(self, assets): '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, @@ -109,6 +179,7 @@ def finalize_assets(self, assets): 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, @@ -119,6 +190,11 @@ def finalize_assets(self, assets): 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), @@ -146,9 +222,15 @@ def finalize_assets(self, assets): 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"], @@ -156,6 +238,7 @@ def finalize_assets(self, assets): 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"], @@ -163,6 +246,7 @@ def finalize_assets(self, assets): 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}) @@ -173,6 +257,7 @@ def finalize_assets(self, assets): 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}) @@ -186,6 +271,7 @@ def finalize_assets(self, assets): '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"], @@ -199,37 +285,54 @@ def finalize_assets(self, assets): 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'}) @@ -238,8 +341,11 @@ def finalize_assets(self, assets): 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}) @@ -248,13 +354,24 @@ def finalize_assets(self, assets): 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)), + cube = nw.new_node(Nodes.MeshCube, input_kwargs={ 'Size': group_input.outputs["Size"], 'Vertices X': group_input.outputs["Resolution"], @@ -268,6 +385,7 @@ def finalize_assets(self, assets): 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'}) @@ -287,6 +405,23 @@ def finalize_assets(self, assets): +@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)), + ('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'}) @@ -302,6 +437,7 @@ def finalize_assets(self, assets): 'Z': subtract_1 }) + cube_2 = nw.new_node(Nodes.MeshCube, store_named_attribute_1 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube_2.outputs["Mesh"], @@ -312,22 +448,28 @@ def finalize_assets(self, assets): 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'}) @@ -338,6 +480,7 @@ def finalize_assets(self, assets): 'Z': group_input.outputs["Thickness"] }) + cube_1 = nw.new_node(Nodes.MeshCube, store_named_attribute_4 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube_1.outputs["Mesh"], @@ -354,10 +497,12 @@ def finalize_assets(self, assets): 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'}) @@ -368,6 +513,7 @@ def finalize_assets(self, assets): 'Z': group_input.outputs["Thickness"] }) + cube = nw.new_node(Nodes.MeshCube, store_named_attribute = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube.outputs["Mesh"], @@ -381,11 +527,14 @@ def finalize_assets(self, assets): 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'}) @@ -399,6 +548,7 @@ def finalize_assets(self, assets): 'Z': subtract_7 }) + cube_3 = nw.new_node(Nodes.MeshCube, store_named_attribute_5 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube_3.outputs["Mesh"], @@ -412,13 +562,16 @@ def finalize_assets(self, assets): 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"], @@ -426,6 +579,7 @@ def finalize_assets(self, assets): 'Z': separate_xyz.outputs["Z"] }) + cube_4 = nw.new_node(Nodes.MeshCube, store_named_attribute_2 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube_4.outputs["Mesh"], @@ -436,14 +590,17 @@ def finalize_assets(self, assets): 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"], @@ -451,6 +608,7 @@ def finalize_assets(self, assets): 'Z': separate_xyz.outputs["Z"] }) + cube_5 = nw.new_node(Nodes.MeshCube, store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute, input_kwargs={ 'Geometry': cube_5.outputs["Mesh"], @@ -467,10 +625,12 @@ def finalize_assets(self, assets): 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], @@ -481,6 +641,10 @@ def finalize_assets(self, assets): attrs={'is_active_output': True}) + + # Code generated using version 2.6.5 of the node_transpiler + + combine_xyz = nw.new_node(Nodes.CombineXYZ, input_kwargs={ 'X': group_input.outputs["Depth"], 'Y': group_input.outputs["Width"], @@ -503,8 +667,11 @@ def finalize_assets(self, assets): '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, @@ -523,7 +690,9 @@ def finalize_assets(self, assets): 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, @@ -535,6 +704,7 @@ def finalize_assets(self, assets): 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, @@ -549,6 +719,7 @@ def finalize_assets(self, assets): 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'}) @@ -559,12 +730,14 @@ def finalize_assets(self, assets): 'Size': multiply_6 }) + set_material_9 = nw.new_node(Nodes.SetMaterial, geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) + 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, @@ -572,6 +745,7 @@ def finalize_assets(self, assets): 'Pivot Point': combine_xyz_4 }) + 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'}) @@ -586,11 +760,14 @@ def finalize_assets(self, assets): 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, @@ -603,6 +780,7 @@ def finalize_assets(self, assets): 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'}) @@ -610,8 +788,11 @@ def finalize_assets(self, assets): 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}) @@ -622,23 +803,33 @@ def finalize_assets(self, assets): }) + 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}) + 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}) From 3be5457dfd02050561fd781b1a0c95d28957367a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 557/727] Add 57 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/appliances/dishwasher.py | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/infinigen/assets/appliances/dishwasher.py b/infinigen/assets/appliances/dishwasher.py index 1ca0aae6f..ab5c5c55d 100644 --- a/infinigen/assets/appliances/dishwasher.py +++ b/infinigen/assets/appliances/dishwasher.py @@ -18,6 +18,7 @@ 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.]): @@ -26,7 +27,35 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): 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 @@ -644,6 +673,20 @@ 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=[('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"], @@ -658,6 +701,9 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 'Switch4': True }) + set_material_1 = nw.new_node(Nodes.SetMaterial, + input_kwargs={'Geometry': hollowcube, 'Material': group_input.outputs["Surface"]}) + @@ -681,6 +727,11 @@ def nodegroup_hollow_cube(nw: NodeWrangler): '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"]}) @@ -712,6 +763,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): '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"]}) @@ -802,6 +855,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 'Offset': combine_xyz_5 }) + set_material = nw.new_node(Nodes.SetMaterial, + input_kwargs={'Geometry': set_position, 'Material': group_input.outputs["Surface"]}) racks = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material}, label='racks') @@ -823,6 +878,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}) From d585eb1197798a1ad22a33f9640261c52fa99efa Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:29 -0700 Subject: [PATCH 558/727] Add 37 lines to infinigen/assets/appliances/dishwasher.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/assets/appliances/dishwasher.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/infinigen/assets/appliances/dishwasher.py b/infinigen/assets/appliances/dishwasher.py index ab5c5c55d..0b1694a3b 100644 --- a/infinigen/assets/appliances/dishwasher.py +++ b/infinigen/assets/appliances/dishwasher.py @@ -13,6 +13,8 @@ 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 @@ -82,8 +84,14 @@ def sample_parameters(dimensions): 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): @@ -400,6 +408,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"], @@ -441,6 +450,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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), @@ -467,6 +477,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -510,6 +521,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -543,6 +555,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -578,6 +591,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -609,6 +623,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -638,6 +653,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): }) 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"], @@ -671,6 +687,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): +@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, @@ -704,8 +722,11 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"], @@ -734,6 +755,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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'}) @@ -783,12 +805,21 @@ def nodegroup_hollow_cube(nw: NodeWrangler): '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"]}) @@ -798,6 +829,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): '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}, @@ -858,6 +891,8 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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, @@ -881,7 +916,9 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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') From 38d322e051c35924899dc741848530deea3a74e6 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 559/727] Add 262 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/appliances/microwave.py | 262 +++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 infinigen/assets/appliances/microwave.py diff --git a/infinigen/assets/appliances/microwave.py b/infinigen/assets/appliances/microwave.py new file mode 100644 index 000000000..063a04a6d --- /dev/null +++ b/infinigen/assets/appliances/microwave.py @@ -0,0 +1,262 @@ +# Copyright (c) Princeton University. +# This 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 import surface +from infinigen.core.util import blender as butil +from infinigen.core.util.math import FixedSeed +from infinigen.core.placement.factory import AssetFactory + +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) + + @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 + 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() + + +@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)), + 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}) + + # 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), + 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, + 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, + 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"]}) + 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"]}) + 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, + set_material_2 = nw.new_node(Nodes.SetMaterial, + 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, + 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}) + 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, + convex_hull_1 = nw.new_node(Nodes.ConvexHull, input_kwargs={'Geometry': separate_geometry.outputs["Inverted"]}) + 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, + set_material_5 = nw.new_node(Nodes.SetMaterial, + 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, + join_geometry = nw.new_node(Nodes.JoinGeometry, + input_kwargs={'Geometry': [set_material_1, rotate_instances, set_material, set_material_5, text_1]}) From fe264773094c1b1dcd3a065e83603dcdceecd737 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 560/727] Add 118 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/appliances/microwave.py | 118 +++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/infinigen/assets/appliances/microwave.py b/infinigen/assets/appliances/microwave.py index 063a04a6d..ee1158734 100644 --- a/infinigen/assets/appliances/microwave.py +++ b/infinigen/assets/appliances/microwave.py @@ -7,6 +7,8 @@ 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 @@ -15,6 +17,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.core.placement.factory import AssetFactory + class MicrowaveFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): super(MicrowaveFactory, self).__init__(factory_seed, coarse=coarse) @@ -33,6 +36,7 @@ def sample_parameters(dimensions): 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, @@ -45,8 +49,16 @@ def sample_parameters(dimensions): "BrandName": brand_name, } return params + def create_asset(self, **params): obj = butil.spawn_cube() + butil.modify_mesh(obj, 'NODES', node_group=nodegroup_microwave_geometry(), ng_inputs=self.params, apply=True) + + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) @node_utils.to_nodegroup('nodegroup_plate', singleton=False, type='GeometryNodeTree') @@ -54,12 +66,18 @@ 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') @@ -71,14 +89,19 @@ def nodegroup_text(nw: NodeWrangler): ('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') @@ -91,39 +114,57 @@ def nodegroup_center(nw: NodeWrangler): ('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') @@ -133,19 +174,25 @@ def nodegroup_cube(nw: NodeWrangler): 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)), + 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}) # Code generated using version 2.6.5 of the node_transpiler @@ -159,104 +206,175 @@ def nodegroup_cube(nw: NodeWrangler): ('NodeSocketFloat', 'DoorThickness', 0.0000), ('NodeSocketFloat', 'DoorMargin', 0.0500), ('NodeSocketFloat', 'DoorRotation', 0.0000), + 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, + 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, + 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"]}) + + 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"]}) + + 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, + set_material_2 = nw.new_node(Nodes.SetMaterial, + 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, + 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}) + + 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, convex_hull_1 = nw.new_node(Nodes.ConvexHull, input_kwargs={'Geometry': separate_geometry.outputs["Inverted"]}) + + 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, + set_material_5 = nw.new_node(Nodes.SetMaterial, + 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, + 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}) From f0dc342e42fa0c7fae540d74c3e59e87d97fe63b Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 561/727] Add 41 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/appliances/microwave.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/infinigen/assets/appliances/microwave.py b/infinigen/assets/appliances/microwave.py index ee1158734..c52f577c3 100644 --- a/infinigen/assets/appliances/microwave.py +++ b/infinigen/assets/appliances/microwave.py @@ -2,6 +2,7 @@ # This 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 @@ -16,6 +17,7 @@ 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 MicrowaveFactory(AssetFactory): @@ -25,7 +27,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): 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) @@ -57,7 +86,9 @@ def create_asset(self, **params): return obj def finalize_assets(self, assets): + if self.scratch: self.scratch.apply(assets) + if self.edge_wear: self.edge_wear.apply(assets) @@ -206,6 +237,9 @@ def nodegroup_cube(nw: NodeWrangler): ('NodeSocketFloat', 'DoorThickness', 0.0000), ('NodeSocketFloat', 'DoorMargin', 0.0500), ('NodeSocketFloat', 'DoorRotation', 0.0000), + ('NodeSocketMaterial', 'Surface', None), + ('NodeSocketMaterial', 'Back', None), + ('NodeSocketMaterial', 'BlackGlass', 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"]}) @@ -264,6 +298,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -298,8 +333,10 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -338,6 +375,8 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -347,8 +386,10 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) From b765ac16859bc2d725fc29492d61e7a5dea1f926 Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 562/727] Add 26 lines to infinigen/assets/appliances/microwave.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/assets/appliances/microwave.py | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/infinigen/assets/appliances/microwave.py b/infinigen/assets/appliances/microwave.py index c52f577c3..3e21353b3 100644 --- a/infinigen/assets/appliances/microwave.py +++ b/infinigen/assets/appliances/microwave.py @@ -15,6 +15,8 @@ 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 @@ -80,8 +82,13 @@ def sample_parameters(dimensions): 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 @@ -205,6 +212,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -226,6 +234,8 @@ def nodegroup_cube(nw: NodeWrangler): 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, @@ -237,9 +247,12 @@ def nodegroup_cube(nw: NodeWrangler): ('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"]}) @@ -265,6 +278,7 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -305,6 +319,7 @@ def nodegroup_cube(nw: NodeWrangler): 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) @@ -326,6 +341,7 @@ def nodegroup_cube(nw: NodeWrangler): 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) @@ -357,11 +373,17 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -379,6 +401,7 @@ def nodegroup_cube(nw: NodeWrangler): 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) @@ -414,6 +437,9 @@ def nodegroup_cube(nw: NodeWrangler): 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]}) From 8790bb93019f480af9c639a60a7bd5db8c0e8412 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 563/727] Add 2 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/appliances/__init__.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 infinigen/assets/appliances/__init__.py diff --git a/infinigen/assets/appliances/__init__.py b/infinigen/assets/appliances/__init__.py new file mode 100644 index 000000000..f2b82bd6a --- /dev/null +++ b/infinigen/assets/appliances/__init__.py @@ -0,0 +1,2 @@ +from .oven import OvenFactory +from .beverage_fridge import BeverageFridgeFactory From 03813569e88df338d8386b25239629a0dff2fe99 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 564/727] Add 2 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/appliances/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/appliances/__init__.py b/infinigen/assets/appliances/__init__.py index f2b82bd6a..861eb292c 100644 --- a/infinigen/assets/appliances/__init__.py +++ b/infinigen/assets/appliances/__init__.py @@ -1,2 +1,4 @@ from .oven import OvenFactory from .beverage_fridge import BeverageFridgeFactory +from .microwave import MicrowaveFactory +from .tv import TVFactory, MonitorFactory From b2f177a836ff75f4936b41dab4ee4aca8e28d294 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 565/727] Add 1 lines to infinigen/assets/appliances/__init__.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/appliances/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/appliances/__init__.py b/infinigen/assets/appliances/__init__.py index 861eb292c..795254199 100644 --- a/infinigen/assets/appliances/__init__.py +++ b/infinigen/assets/appliances/__init__.py @@ -1,4 +1,5 @@ from .oven import OvenFactory from .beverage_fridge import BeverageFridgeFactory +from .dishwasher import DishwasherFactory from .microwave import MicrowaveFactory from .tv import TVFactory, MonitorFactory From 393107676c2bbc1e73741e710ce6826744e5492f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 566/727] Add 191 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/appliances/tv.py | 191 ++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 infinigen/assets/appliances/tv.py diff --git a/infinigen/assets/appliances/tv.py b/infinigen/assets/appliances/tv.py new file mode 100644 index 000000000..8d5997610 --- /dev/null +++ b/infinigen/assets/appliances/tv.py @@ -0,0 +1,191 @@ +# Copyright (c) Princeton University. + +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 + + +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.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'] + + + 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) + 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' From 373333b25cb4c9a03912cd8e912198687486a729 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 567/727] Add 21 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/appliances/tv.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/infinigen/assets/appliances/tv.py b/infinigen/assets/appliances/tv.py index 8d5997610..a016c5b89 100644 --- a/infinigen/assets/appliances/tv.py +++ b/infinigen/assets/appliances/tv.py @@ -16,6 +16,8 @@ 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): @@ -46,7 +48,26 @@ def __init__(self, factory_seed, coarse=False): 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 From 4fb0f35ed6389bf566027c42b37ca4e746063c3b Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 568/727] Add 5 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/appliances/tv.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/appliances/tv.py b/infinigen/assets/appliances/tv.py index a016c5b89..481da94c5 100644 --- a/infinigen/assets/appliances/tv.py +++ b/infinigen/assets/appliances/tv.py @@ -1,4 +1,7 @@ # Copyright (c) Princeton University. +# Authors: +# - Lingjie Mei: primary author +# - Karhan Kayan: fix rotation import bpy import bmesh @@ -25,6 +28,7 @@ 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) @@ -109,6 +113,7 @@ def create_asset(self, **params) -> bpy.types.Object: write_attribute(l, 1, 'leg', 'FACE', 'INT') parts.extend(legs) obj = join_objects(parts) + butil.apply_transform(obj) return obj def make_screen(self, obj): From 456911b7e98e50adec2b70d74450f931a2ae2893 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 569/727] Add 4 lines to infinigen/assets/appliances/tv.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/appliances/tv.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/appliances/tv.py b/infinigen/assets/appliances/tv.py index 481da94c5..2a1dcebc6 100644 --- a/infinigen/assets/appliances/tv.py +++ b/infinigen/assets/appliances/tv.py @@ -1,4 +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: primary author # - Karhan Kayan: fix rotation @@ -113,6 +116,7 @@ def create_asset(self, **params) -> bpy.types.Object: 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 From a9484ccd3b1b835f4bffd1388f3efce4342f3ab7 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 570/727] Add 626 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/appliances/oven.py | 626 ++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 infinigen/assets/appliances/oven.py diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py new file mode 100644 index 000000000..08c475d25 --- /dev/null +++ b/infinigen/assets/appliances/oven.py @@ -0,0 +1,626 @@ +# 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 import surface +from infinigen.core.util import blender as butil + +from infinigen.core.util.math import FixedSeed +from infinigen.core.placement.factory import AssetFactory + +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): + + @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) + + params = { + "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, + } + def create_asset(self, **params): + obj = butil.spawn_cube() +@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)), + ('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, + 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, + 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, + 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, + 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, + 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, + 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), + 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, + 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)), + 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}) + + + # 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), + 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, + set_material_3 = nw.new_node(Nodes.SetMaterial, + 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, + 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}) + set_material_9 = nw.new_node(Nodes.SetMaterial, + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) + 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}) + 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, + 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, + 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}) + 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, + set_material_7 = nw.new_node(Nodes.SetMaterial, + 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}) + 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, + botton = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material_6}, label='botton') + 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"]}) + rotate_instances_1 = nw.new_node(Nodes.RotateInstances, + 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, + join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [door, racks, heater_1, panel, body]}) + group_output = nw.new_node(Nodes.GroupOutput, From bbbb86bee998a8179ed41f24f34ae5759608aeef Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 571/727] Add 336 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/appliances/oven.py | 336 ++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py index 08c475d25..e3cc1b28b 100644 --- a/infinigen/assets/appliances/oven.py +++ b/infinigen/assets/appliances/oven.py @@ -1,3 +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 import bpy @@ -5,6 +8,8 @@ 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 @@ -40,6 +45,7 @@ def sample_parameters(dimensions): 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() params = { "Depth": depth, @@ -60,6 +66,12 @@ def sample_parameters(dimensions): } def create_asset(self, **params): obj = butil.spawn_cube() + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) + @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 @@ -74,99 +86,167 @@ def nodegroup_hollow_cube(nw: NodeWrangler): ('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, + 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, + 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, + 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, + 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, + 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, + 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') @@ -174,11 +254,16 @@ 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') @@ -186,50 +271,75 @@ 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), + 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, + 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}) @@ -242,44 +352,67 @@ def nodegroup_oven_rack(nw: NodeWrangler): ('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') @@ -291,14 +424,19 @@ def nodegroup_text(nw: NodeWrangler): ('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') @@ -309,33 +447,54 @@ def nodegroup_handle(nw: NodeWrangler): 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') @@ -348,39 +507,57 @@ def nodegroup_center(nw: NodeWrangler): ('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') @@ -390,19 +567,25 @@ def nodegroup_cube(nw: NodeWrangler): 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)), + 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}) @@ -423,204 +606,357 @@ def nodegroup_cube(nw: NodeWrangler): ('NodeSocketFloatDistance', 'BottonRadius', 0.0500), ('NodeSocketFloat', 'BottonThickness', 0.0300), ('NodeSocketFloat', 'HeaterRadiusRatio', 0.1500), + 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, + set_material_3 = nw.new_node(Nodes.SetMaterial, + + 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, + 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}) + set_material_9 = nw.new_node(Nodes.SetMaterial, + + geometry_to_instance = nw.new_node('GeometryNodeGeometryToInstance', input_kwargs={'Geometry': join_geometry_3}) + + 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}) + 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, + 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, + + 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}) + + 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, + set_material_7 = nw.new_node(Nodes.SetMaterial, + + 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}) + + 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, + botton = nw.new_node(Nodes.Reroute, input_kwargs={'Input': set_material_6}, label='botton') + 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"]}) + rotate_instances_1 = nw.new_node(Nodes.RotateInstances, + 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, + + + + join_geometry = nw.new_node(Nodes.JoinGeometry, input_kwargs={'Geometry': [door, racks, heater_1, panel, body]}) + + group_output = nw.new_node(Nodes.GroupOutput, + input_kwargs={'Geometry': geometry}) From 613ccf89cd57b3ad2f80c0226285d9c50fa6a0ba Mon Sep 17 00:00:00 2001 From: Zeyu Ma Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 572/727] Add 172 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. --- infinigen/assets/appliances/oven.py | 172 ++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py index e3cc1b28b..b01d174b5 100644 --- a/infinigen/assets/appliances/oven.py +++ b/infinigen/assets/appliances/oven.py @@ -13,6 +13,8 @@ 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 @@ -25,6 +27,9 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): self.dimensions = dimensions with FixedSeed(factory_seed): + self.params, self.geometry_node_params = self.sample_parameters(dimensions) + self.geometry_node_params.update(self.material_params) + @staticmethod def sample_parameters(dimensions): @@ -47,7 +52,21 @@ def sample_parameters(dimensions): 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, @@ -64,14 +83,126 @@ def sample_parameters(dimensions): "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_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): self.scratch.apply(assets) 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 @@ -79,6 +210,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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), @@ -100,6 +232,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -135,6 +268,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -158,6 +292,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -183,6 +318,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -206,6 +342,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -227,6 +364,7 @@ def nodegroup_hollow_cube(nw: NodeWrangler): 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"]}, @@ -567,6 +705,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -589,6 +728,7 @@ def nodegroup_cube(nw: NodeWrangler): 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') # Code generated using version 2.6.5 of the node_transpiler group_input = nw.new_node(Nodes.GroupInput, @@ -622,6 +762,7 @@ def nodegroup_cube(nw: NodeWrangler): set_material_3 = nw.new_node(Nodes.SetMaterial, + # 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'}) @@ -660,17 +801,27 @@ def nodegroup_cube(nw: NodeWrangler): 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, + 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, @@ -725,6 +876,8 @@ def nodegroup_cube(nw: NodeWrangler): set_material = nw.new_node(Nodes.SetMaterial, + 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"]}) @@ -745,6 +898,7 @@ def nodegroup_cube(nw: NodeWrangler): set_material_5 = nw.new_node(Nodes.SetMaterial, + # 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"]}, @@ -763,6 +917,12 @@ def nodegroup_cube(nw: NodeWrangler): 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') @@ -786,6 +946,7 @@ def nodegroup_cube(nw: NodeWrangler): set_material_7 = nw.new_node(Nodes.SetMaterial, + # 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"]}) @@ -810,6 +971,10 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -934,6 +1099,8 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -942,6 +1109,8 @@ def nodegroup_cube(nw: NodeWrangler): rotate_instances_1 = nw.new_node(Nodes.RotateInstances, + 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, @@ -952,8 +1121,11 @@ def nodegroup_cube(nw: NodeWrangler): set_material_1 = nw.new_node(Nodes.SetMaterial, + 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]}) From 71b86a4bbc04ce99bf3a0716b20c334025ad1be2 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 573/727] Add 48 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/appliances/oven.py | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py index b01d174b5..ec194c485 100644 --- a/infinigen/assets/appliances/oven.py +++ b/infinigen/assets/appliances/oven.py @@ -20,6 +20,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.core.placement.factory import AssetFactory +from infinigen.assets.material_assignments import AssetList class OvenFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): @@ -28,8 +29,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): 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): @@ -131,7 +158,9 @@ def create_asset(self, **params): 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): @@ -414,6 +443,8 @@ def nodegroup_heater(nw: NodeWrangler): 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"]}, @@ -428,7 +459,9 @@ def nodegroup_heater(nw: NodeWrangler): 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}) @@ -746,6 +779,11 @@ def nodegroup_cube(nw: NodeWrangler): ('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), 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"]}) @@ -759,8 +797,10 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -789,6 +829,7 @@ def nodegroup_cube(nw: NodeWrangler): 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"]}) @@ -804,6 +845,7 @@ def nodegroup_cube(nw: NodeWrangler): 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) @@ -875,6 +917,7 @@ def nodegroup_cube(nw: NodeWrangler): 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]) @@ -897,6 +940,7 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -943,8 +987,10 @@ def nodegroup_cube(nw: NodeWrangler): 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}) @@ -1096,6 +1142,7 @@ def nodegroup_cube(nw: NodeWrangler): 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') @@ -1120,6 +1167,7 @@ def nodegroup_cube(nw: NodeWrangler): 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}) From e02a0414b205f6ef3f36dbdc9ce007b30828b158 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 574/727] Add 32 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/appliances/oven.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py index ec194c485..c942bb30e 100644 --- a/infinigen/assets/appliances/oven.py +++ b/infinigen/assets/appliances/oven.py @@ -21,6 +21,11 @@ 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.]): @@ -119,6 +124,16 @@ def sample_parameters(dimensions): "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() + 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) @@ -762,6 +777,7 @@ def nodegroup_cube(nw: NodeWrangler): @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, @@ -784,6 +800,9 @@ def nodegroup_cube(nw: NodeWrangler): ('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"]}) @@ -791,6 +810,7 @@ def nodegroup_cube(nw: NodeWrangler): 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, @@ -1154,7 +1174,12 @@ def nodegroup_cube(nw: NodeWrangler): 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]) @@ -1177,6 +1202,13 @@ def nodegroup_cube(nw: NodeWrangler): 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}) From cc924a340c6d5f1b2f572d4a09fd44ebb1c5c78f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 575/727] Add 1 lines to infinigen/assets/appliances/oven.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/appliances/oven.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/appliances/oven.py b/infinigen/assets/appliances/oven.py index c942bb30e..16eeff2da 100644 --- a/infinigen/assets/appliances/oven.py +++ b/infinigen/assets/appliances/oven.py @@ -133,6 +133,7 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object: # 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() From 60bb83ef26858c3826941ae7ad2f9a1aa7208225 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 576/727] Add 333 lines to infinigen/assets/organizer/plate_rack.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/organizer/plate_rack.py | 333 +++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 infinigen/assets/organizer/plate_rack.py diff --git a/infinigen/assets/organizer/plate_rack.py b/infinigen/assets/organizer/plate_rack.py new file mode 100644 index 000000000..b23fd498f --- /dev/null +++ b/infinigen/assets/organizer/plate_rack.py @@ -0,0 +1,333 @@ +# Copyright (c) Princeton University. +# This 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 + + +@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, + + 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 From 52d093372e3c2ed35186d8dbe2283020520d72fc Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 577/727] Add 2 lines to infinigen/assets/organizer/plate_rack.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/organizer/plate_rack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/organizer/plate_rack.py b/infinigen/assets/organizer/plate_rack.py index b23fd498f..3894741fa 100644 --- a/infinigen/assets/organizer/plate_rack.py +++ b/infinigen/assets/organizer/plate_rack.py @@ -15,6 +15,7 @@ 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') @@ -216,6 +217,7 @@ def plate_geometry_nodes(nw: NodeWrangler, **kwargs): 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}) From c954d37c7de9b57dfadbb362136d47d39d87bb73 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 578/727] Add 384 lines to infinigen/assets/organizer/hook.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/organizer/hook.py | 384 +++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 infinigen/assets/organizer/hook.py diff --git a/infinigen/assets/organizer/hook.py b/infinigen/assets/organizer/hook.py new file mode 100644 index 000000000..9f64aadaf --- /dev/null +++ b/infinigen/assets/organizer/hook.py @@ -0,0 +1,384 @@ +# Copyright (c) Princeton University. +# This 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 + + +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, + + 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, + + 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 + + + + + + + From 88777a031d30bb21fc5c3c806de59135403c382e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 579/727] Add 3 lines to infinigen/assets/organizer/hook.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/organizer/hook.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/organizer/hook.py b/infinigen/assets/organizer/hook.py index 9f64aadaf..2403dd132 100644 --- a/infinigen/assets/organizer/hook.py +++ b/infinigen/assets/organizer/hook.py @@ -14,6 +14,7 @@ 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): @@ -123,6 +124,7 @@ def hook_geometry_nodes(nw: NodeWrangler, **kwargs): 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]}) @@ -209,6 +211,7 @@ def spatula_geometry_nodes(nw: NodeWrangler, **kwargs): 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}) From 0dae372fc3e98193bcf8e8b55041f0d9837e808e Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 580/727] Add 9 lines to infinigen/assets/organizer/__init__.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/organizer/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 infinigen/assets/organizer/__init__.py 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 From fbed1ece83936fc6cfca2f6aecca7563aae6ad44 Mon Sep 17 00:00:00 2001 From: Beining Han Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 581/727] Add 304 lines to infinigen/assets/organizer/basket.py. Contributed as part of Infinigen-Indoors by Beining Han. --- infinigen/assets/organizer/basket.py | 304 +++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 infinigen/assets/organizer/basket.py diff --git a/infinigen/assets/organizer/basket.py b/infinigen/assets/organizer/basket.py new file mode 100644 index 000000000..30f913cb7 --- /dev/null +++ b/infinigen/assets/organizer/basket.py @@ -0,0 +1,304 @@ +# Copyright (c) Princeton University. +# This 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 + + +@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, + + 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 From aa0f40e664e843d82daa84c84f1823dc322d42f9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 582/727] Add 2 lines to infinigen/assets/organizer/basket.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/organizer/basket.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/organizer/basket.py b/infinigen/assets/organizer/basket.py index 30f913cb7..5e711c113 100644 --- a/infinigen/assets/organizer/basket.py +++ b/infinigen/assets/organizer/basket.py @@ -14,6 +14,7 @@ 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') @@ -240,6 +241,7 @@ def geometry_nodes(nw: NodeWrangler, **kwargs): 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}) From 250bfa4d4d01d9fb4b0d54dd770afdb44b36cac5 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 583/727] Add 157 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/wall_decorations/range_hood.py | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 infinigen/assets/wall_decorations/range_hood.py diff --git a/infinigen/assets/wall_decorations/range_hood.py b/infinigen/assets/wall_decorations/range_hood.py new file mode 100644 index 000000000..1e63d08f9 --- /dev/null +++ b/infinigen/assets/wall_decorations/range_hood.py @@ -0,0 +1,157 @@ +# 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 + + +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) + + @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) + + +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, + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': transform_geometry_6}, attrs={'is_active_output': True}) + From f4a1ace54f2f489695a95e32246cd5cf53429f18 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 584/727] Add 20 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/wall_decorations/range_hood.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/infinigen/assets/wall_decorations/range_hood.py b/infinigen/assets/wall_decorations/range_hood.py index 1e63d08f9..5a5320e52 100644 --- a/infinigen/assets/wall_decorations/range_hood.py +++ b/infinigen/assets/wall_decorations/range_hood.py @@ -16,6 +16,7 @@ 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): @@ -27,6 +28,22 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): with FixedSeed(factory_seed): self.params = self.sample_parameters(dimensions) + + def get_material_params(self): + material_assignments = AssetList['RangeHoodFactory']() + + 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 + + @staticmethod def sample_parameters(dimensions): # all in meters @@ -63,6 +80,9 @@ def create_asset(self, **params): butil.modify_mesh(obj, 'SOLIDIFY', apply=True, thickness=.002) butil.modify_mesh(obj, 'SUBSURF', apply=True, levels=1, render_levels=1) + if self.scratch: + if self.edge_wear: + def geometry_generate_hood(nw: NodeWrangler, **kwargs): # Code generated using version 2.6.4 of the node_transpiler From 89518e57e78fa03b9a0206a77b69d49f5a9855d7 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:30 -0700 Subject: [PATCH 585/727] Add 14 lines to infinigen/assets/wall_decorations/range_hood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/wall_decorations/range_hood.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/infinigen/assets/wall_decorations/range_hood.py b/infinigen/assets/wall_decorations/range_hood.py index 5a5320e52..2cbbfd7b8 100644 --- a/infinigen/assets/wall_decorations/range_hood.py +++ b/infinigen/assets/wall_decorations/range_hood.py @@ -1,3 +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: Yiming Zuo import bpy @@ -27,10 +30,12 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): 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'] @@ -43,6 +48,7 @@ def get_material_params(self): if not is_edge_wear: edge_wear = None + return surface, scratch, edge_wear @staticmethod def sample_parameters(dimensions): @@ -80,9 +86,16 @@ def create_asset(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 @@ -172,6 +185,7 @@ def geometry_range_hood(nw: NodeWrangler): 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}) From 95da26570d082935194e6e8fb6900da33ba070c4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 586/727] Add 126 lines to infinigen/assets/wall_decorations/wall_shelf.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/wall_decorations/wall_shelf.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 infinigen/assets/wall_decorations/wall_shelf.py 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 From 1f486e7a5373b600c29a2ff4302c859897c42735 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 587/727] Add 8 lines to infinigen/assets/wall_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/wall_decorations/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 infinigen/assets/wall_decorations/__init__.py 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 From 60b8fcaf36ee60c89bf3b1926da37790bbd5daaf Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 588/727] Add 73 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/wall_decorations/wall_art.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 infinigen/assets/wall_decorations/wall_art.py diff --git a/infinigen/assets/wall_decorations/wall_art.py b/infinigen/assets/wall_decorations/wall_art.py new file mode 100644 index 000000000..cd066fabe --- /dev/null +++ b/infinigen/assets/wall_decorations/wall_art.py @@ -0,0 +1,73 @@ +# Copyright (c) Princeton University. +# This 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 + + +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.assign_materials() + + def assign_materials(self): + 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 + + return new_bbox( + 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() + 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.assign_materials() From bd3b749a871dcafab65c9164718f53d132fa483e Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 589/727] Add 12 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/wall_decorations/wall_art.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/assets/wall_decorations/wall_art.py b/infinigen/assets/wall_decorations/wall_art.py index cd066fabe..4078cd180 100644 --- a/infinigen/assets/wall_decorations/wall_art.py +++ b/infinigen/assets/wall_decorations/wall_art.py @@ -40,11 +40,22 @@ def assign_materials(self): 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, + -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() @@ -55,6 +66,7 @@ def assign_materials(self): 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) From 1e8dd2034601839285b23cfcb8c7efa464a3e07c Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 590/727] Add 4 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/wall_decorations/wall_art.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/wall_decorations/wall_art.py b/infinigen/assets/wall_decorations/wall_art.py index 4078cd180..71e61daca 100644 --- a/infinigen/assets/wall_decorations/wall_art.py +++ b/infinigen/assets/wall_decorations/wall_art.py @@ -14,6 +14,7 @@ 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): @@ -27,9 +28,11 @@ def __init__(self, factory_seed, coarse=False): 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: @@ -82,4 +85,5 @@ def finalize_assets(self, 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() From f7cf050589690fabfae8f79a470eb5a30f4976ea Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 591/727] Add 1 lines to infinigen/assets/wall_decorations/wall_art.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/wall_decorations/wall_art.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/wall_decorations/wall_art.py b/infinigen/assets/wall_decorations/wall_art.py index 71e61daca..8e2cc7497 100644 --- a/infinigen/assets/wall_decorations/wall_art.py +++ b/infinigen/assets/wall_decorations/wall_art.py @@ -47,6 +47,7 @@ def assign_materials(self): 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, From 3fd45bf2a774d374d679a54ca80695503ad88529 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 592/727] Add 200 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/wall_decorations/skirting_board.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 infinigen/assets/wall_decorations/skirting_board.py diff --git a/infinigen/assets/wall_decorations/skirting_board.py b/infinigen/assets/wall_decorations/skirting_board.py new file mode 100644 index 000000000..8018d530b --- /dev/null +++ b/infinigen/assets/wall_decorations/skirting_board.py @@ -0,0 +1,200 @@ +# Copyright (c) Princeton University. +# 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 bmesh +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.surface import write_attr_data +from infinigen.assets.utils.shapes import polygon2obj, obj2polygon +from shapely.plotting import plot_polygon + + group_input = nw.new_node( + Nodes.GroupInput, + ('NodeSocketBool', 'Is Ceiling', False)] + ) + + + 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'} + ) + + + transform_geometry = nw.new_node( + Nodes.Transform, input_kwargs={'Geometry': quadrilateral, 'Translation': combine_xyz} + ) + + resample_curve_1 = nw.new_node( + Nodes.ResampleCurve, + attrs={'mode': 'LENGTH'} + ) + + + + + multiply_2 = nw.new_node( + Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: -1.0000}, attrs={'operation': 'MULTIPLY'} + ) + + + + multiply_3 = nw.new_node( + Nodes.Math, + attrs={'operation': 'MULTIPLY'} + ) + + + 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): + thickness = uniform(.02, .05) + control_points = [(0.0000, start_y)] + 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): + tagging.extract_tagged_faces(o, {tag, t.Subpart.Visible}, nonempty=True) + 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') + + boundaries = [boundary] + 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) + 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) From 7a8d5b176a66819f73ab51242928fa92e94ee12b Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 593/727] Add 67 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/wall_decorations/skirting_board.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/infinigen/assets/wall_decorations/skirting_board.py b/infinigen/assets/wall_decorations/skirting_board.py index 8018d530b..8ba565cda 100644 --- a/infinigen/assets/wall_decorations/skirting_board.py +++ b/infinigen/assets/wall_decorations/skirting_board.py @@ -1,7 +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. + import bmesh +import bpy +import mathutils +import numpy as np +from numpy.random import uniform, normal, randint, choice, randint 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, @@ -12,15 +17,39 @@ 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 + +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 shapely.plotting import plot_polygon + +@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}) @@ -38,6 +67,7 @@ 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} @@ -45,23 +75,32 @@ 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, @@ -92,9 +131,25 @@ 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)] @@ -117,18 +172,27 @@ def apply_skirtingboard(nw: NodeWrangler, contour, is_ceiling=False, seed=None, ) } ) + 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 + tagging.extract_tagged_faces(o, {tag, t.Subpart.Visible}, nonempty=True) + all_polys = [] + all_zs = [] 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 = [] @@ -145,11 +209,14 @@ def make_skirtingboard_contour(objs: list[bpy.types.Object], tag: t.Subpart): o.location[-1] += floor_z butil.apply_transform(o, True) 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=}') From 549c30fac87bb9060e33fa228073d2769fc5095e Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 594/727] Add 18 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../assets/wall_decorations/skirting_board.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infinigen/assets/wall_decorations/skirting_board.py b/infinigen/assets/wall_decorations/skirting_board.py index 8ba565cda..82f82551f 100644 --- a/infinigen/assets/wall_decorations/skirting_board.py +++ b/infinigen/assets/wall_decorations/skirting_board.py @@ -1,12 +1,17 @@ # Copyright (c) Princeton University. # This 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, @@ -29,12 +34,16 @@ 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 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): @@ -181,13 +190,21 @@ def apply_skirtingboard(nw: NodeWrangler, contour, is_ceiling=False, seed=None, 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): @@ -208,6 +225,7 @@ def make_skirtingboard_contour(objs: list[bpy.types.Object], tag: t.Subpart): contours.append(o) o.location[-1] += floor_z butil.apply_transform(o, True) + butil.delete(objs) return contours From 71c2d1ad00089c08e45badce7d5a31563d436f1d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 595/727] Add 1 lines to infinigen/assets/wall_decorations/skirting_board.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/wall_decorations/skirting_board.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/wall_decorations/skirting_board.py b/infinigen/assets/wall_decorations/skirting_board.py index 82f82551f..ce85f9f30 100644 --- a/infinigen/assets/wall_decorations/skirting_board.py +++ b/infinigen/assets/wall_decorations/skirting_board.py @@ -40,6 +40,7 @@ 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__) From 5f5b57bb9808793ec7d19aa58502bff1a3fec37f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 596/727] Add 74 lines to infinigen/assets/wall_decorations/balloon.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/wall_decorations/balloon.py | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infinigen/assets/wall_decorations/balloon.py diff --git a/infinigen/assets/wall_decorations/balloon.py b/infinigen/assets/wall_decorations/balloon.py new file mode 100644 index 000000000..bcb3652d1 --- /dev/null +++ b/infinigen/assets/wall_decorations/balloon.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: 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 + + +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) + 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) From 6104869aa54b2f9e5885161ecbc90727547959e3 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 597/727] Add 3 lines to infinigen/assets/wall_decorations/balloon.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/wall_decorations/balloon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/wall_decorations/balloon.py b/infinigen/assets/wall_decorations/balloon.py index bcb3652d1..49da510ca 100644 --- a/infinigen/assets/wall_decorations/balloon.py +++ b/infinigen/assets/wall_decorations/balloon.py @@ -16,6 +16,7 @@ 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): @@ -25,6 +26,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.displace = uniform(.02, .04) def create_placeholder(self, **kwargs) -> bpy.types.Object: From ba06ebd1b3b581b848f9b5738a1bdb50547fb08f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 598/727] Add 1 lines to infinigen/assets/wall_decorations/balloon.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/wall_decorations/balloon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/wall_decorations/balloon.py b/infinigen/assets/wall_decorations/balloon.py index 49da510ca..bb8cc032f 100644 --- a/infinigen/assets/wall_decorations/balloon.py +++ b/infinigen/assets/wall_decorations/balloon.py @@ -28,6 +28,7 @@ def __init__(self, factory_seed, coarse=False): 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: From 5fc27ba8480e93428f92788655f9a943fea6a4ee Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 599/727] Add 87 lines to infinigen/assets/utils/bbox_from_mesh.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/utils/bbox_from_mesh.py | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 infinigen/assets/utils/bbox_from_mesh.py 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 From 514f70c0f158b75787cc42cef7526b2ce17a9dee Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 600/727] Add 142 lines to infinigen/assets/utils/shapes.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/utils/shapes.py | 142 +++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 infinigen/assets/utils/shapes.py 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() From 61d736ddfcfe1c572e3fae1ef3cedc6c85a9cdff Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 601/727] Add 53 lines to infinigen/assets/utils/autobevel.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/utils/autobevel.py | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 infinigen/assets/utils/autobevel.py 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 From be60a34f8051cedf27f9e7752ddba5702efac52f Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 602/727] Add 43 lines to infinigen/assets/utils/extract_nodegroup_parts.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../assets/utils/extract_nodegroup_parts.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 infinigen/assets/utils/extract_nodegroup_parts.py 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 From 75f82ffebeeb701c7e1e823c4f480cc578a9cc77 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 603/727] Add 193 lines to infinigen/assets/utils/uv.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/utils/uv.py | 193 +++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 infinigen/assets/utils/uv.py diff --git a/infinigen/assets/utils/uv.py b/infinigen/assets/utils/uv.py new file mode 100644 index 000000000..e27bd5c1d --- /dev/null +++ b/infinigen/assets/utils/uv.py @@ -0,0 +1,193 @@ +# Copyright (c) Princeton University. +# This 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 + + + +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) + + +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) From a2fc4afc4c56eca68b010432d43601819bbd05e4 Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 604/727] Add 8 lines to infinigen/assets/utils/uv.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/utils/uv.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infinigen/assets/utils/uv.py b/infinigen/assets/utils/uv.py index e27bd5c1d..72e8090a6 100644 --- a/infinigen/assets/utils/uv.py +++ b/infinigen/assets/utils/uv.py @@ -16,6 +16,9 @@ ) from infinigen.core.util import blender as butil +import logging + +logger = logging.getLogger(__name__) def face_corner2faces(obj): @@ -136,6 +139,11 @@ def wrap_sides(obj, surface, axes, xs, ys, groupings=None, selection=None, **kwa 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): From 85df50d628c74bd592d9300d34b2c5d11d976564 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 605/727] Add 9 lines to infinigen/assets/elements/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 infinigen/assets/elements/__init__.py diff --git a/infinigen/assets/elements/__init__.py b/infinigen/assets/elements/__init__.py new file mode 100644 index 000000000..08c1f9f33 --- /dev/null +++ b/infinigen/assets/elements/__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 .staircases import * +from .doors import * +from .rug import RugFactory +from .warehouses import * +from .pillars import PillarFactory From 99924e1992b4f6d95729d67b9b84654d71dcd317 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 606/727] Add 1 lines to infinigen/assets/elements/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/elements/__init__.py b/infinigen/assets/elements/__init__.py index 08c1f9f33..765c5d999 100644 --- a/infinigen/assets/elements/__init__.py +++ b/infinigen/assets/elements/__init__.py @@ -6,4 +6,5 @@ from .doors import * from .rug import RugFactory from .warehouses import * +from .nature_shelf_trinkets.generate import NatureShelfTrinketsFactory from .pillars import PillarFactory From a0a8ed244efcefc302718bd7f32d006b3cd6b44b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 607/727] Add 58 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/rug.py | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 infinigen/assets/elements/rug.py diff --git a/infinigen/assets/elements/rug.py b/infinigen/assets/elements/rug.py new file mode 100644 index 000000000..66e8092e1 --- /dev/null +++ b/infinigen/assets/elements/rug.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.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 import blender as butil + + +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) + + 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 From 21868380abe0210de0b7ac7805ced5a4fd5aba8c Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 608/727] Add 5 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/elements/rug.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/elements/rug.py b/infinigen/assets/elements/rug.py index 66e8092e1..b369d1c89 100644 --- a/infinigen/assets/elements/rug.py +++ b/infinigen/assets/elements/rug.py @@ -14,6 +14,7 @@ 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 RugFactory(AssetFactory): @@ -27,6 +28,10 @@ def __init__(self, factory_seed, coarse=False): 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: From 4411e939540f6af36bb166a11729f865e34af9f5 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 609/727] Add 1 lines to infinigen/assets/elements/rug.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/rug.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/elements/rug.py b/infinigen/assets/elements/rug.py index b369d1c89..0f5072902 100644 --- a/infinigen/assets/elements/rug.py +++ b/infinigen/assets/elements/rug.py @@ -13,6 +13,7 @@ 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 From 1d9741ee44051030e010e97f6ba13a0ad6192e36 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 610/727] Add 123 lines to infinigen/assets/elements/pillars.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/pillars.py | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 infinigen/assets/elements/pillars.py 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) From 0eefe2f385657c1d33ea90036e6942b2b59ba847 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:31 -0700 Subject: [PATCH 611/727] Add 166 lines to infinigen/assets/elements/warehouses/rack.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/warehouses/rack.py | 166 +++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 infinigen/assets/elements/warehouses/rack.py diff --git a/infinigen/assets/elements/warehouses/rack.py b/infinigen/assets/elements/warehouses/rack.py new file mode 100644 index 000000000..dca1a7b7f --- /dev/null +++ b/infinigen/assets/elements/warehouses/rack.py @@ -0,0 +1,166 @@ +# Copyright (c) Princeton University. +# This 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.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) + 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') From 6f63dca5fdaa5639786335ca270a28197906744e Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 612/727] Add 2 lines to infinigen/assets/elements/warehouses/rack.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/elements/warehouses/rack.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/elements/warehouses/rack.py b/infinigen/assets/elements/warehouses/rack.py index dca1a7b7f..7a7dd2023 100644 --- a/infinigen/assets/elements/warehouses/rack.py +++ b/infinigen/assets/elements/warehouses/rack.py @@ -21,6 +21,7 @@ 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 @@ -54,6 +55,7 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object: 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 From fbee138ef5dd2f313079f8f593b02f3bd9506d7d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 613/727] Add 85 lines to infinigen/assets/elements/warehouses/pallet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/warehouses/pallet.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 infinigen/assets/elements/warehouses/pallet.py diff --git a/infinigen/assets/elements/warehouses/pallet.py b/infinigen/assets/elements/warehouses/pallet.py new file mode 100644 index 000000000..ddd23f509 --- /dev/null +++ b/infinigen/assets/elements/warehouses/pallet.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: 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 + + +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) + '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) From f563349f748f5f0f98eac3cbf32ef1ef40947ebc Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 614/727] Add 2 lines to infinigen/assets/elements/warehouses/pallet.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/elements/warehouses/pallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/elements/warehouses/pallet.py b/infinigen/assets/elements/warehouses/pallet.py index ddd23f509..754ab60d1 100644 --- a/infinigen/assets/elements/warehouses/pallet.py +++ b/infinigen/assets/elements/warehouses/pallet.py @@ -14,6 +14,7 @@ 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): @@ -29,6 +30,7 @@ def __init__(self, factory_seed, coarse=False): 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 From 51f8c59ef8f48543224b78bacab978e619de5d47 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 615/727] Add 6 lines to infinigen/assets/elements/warehouses/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/warehouses/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 infinigen/assets/elements/warehouses/__init__.py 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 From 299d0fa1a1b3ab3e17a8745edc83613ff1b4c605 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 616/727] Add 151 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/u_shaped.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 infinigen/assets/elements/staircases/u_shaped.py diff --git a/infinigen/assets/elements/staircases/u_shaped.py b/infinigen/assets/elements/staircases/u_shaped.py new file mode 100644 index 000000000..9e73a076f --- /dev/null +++ b/infinigen/assets/elements/staircases/u_shaped.py @@ -0,0 +1,151 @@ +# Copyright (c) Princeton University. + +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_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 From e09c8e82754915cbc0e589945c7ae55efd74cf8f Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 617/727] Add 4 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/staircases/u_shaped.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/staircases/u_shaped.py b/infinigen/assets/elements/staircases/u_shaped.py index 9e73a076f..978e73174 100644 --- a/infinigen/assets/elements/staircases/u_shaped.py +++ b/infinigen/assets/elements/staircases/u_shaped.py @@ -1,4 +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. + import bpy import numpy as np @@ -21,6 +24,7 @@ def __init__(self, factory_seed, coarse=False): 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) From 2b47343d777278497807e7fc5a520155f54f9bd9 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 618/727] Add 3 lines to infinigen/assets/elements/staircases/u_shaped.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/staircases/u_shaped.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/elements/staircases/u_shaped.py b/infinigen/assets/elements/staircases/u_shaped.py index 978e73174..7c0aeebf1 100644 --- a/infinigen/assets/elements/staircases/u_shaped.py +++ b/infinigen/assets/elements/staircases/u_shaped.py @@ -2,6 +2,9 @@ # This 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 bb83f9fbc9564a016492ccfd6c0f3f43292a5b9d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 619/727] Add 33 lines to infinigen/assets/elements/staircases/generate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/generate.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 infinigen/assets/elements/staircases/generate.py 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) From 00b9daeacd9a9848f74941ba769e3dbd5148e847 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 620/727] Add 145 lines to infinigen/assets/elements/staircases/l_shaped.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/l_shaped.py | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 infinigen/assets/elements/staircases/l_shaped.py 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 From ea2aaff5e119f7295497733c8f47e33a76f47817 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 621/727] Add 18 lines to infinigen/assets/elements/staircases/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 infinigen/assets/elements/staircases/__init__.py 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()) From 985ac3a5dd8d6afe716db0721d77f506bebe1d0a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 622/727] Add 32 lines to infinigen/assets/elements/staircases/cantilever.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/cantilever.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 infinigen/assets/elements/staircases/cantilever.py 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 From 1fe8532fa6b56953c0b09ce5834d8c1aefe37d63 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 623/727] Add 592 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/straight.py | 592 ++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 infinigen/assets/elements/staircases/straight.py diff --git a/infinigen/assets/elements/staircases/straight.py b/infinigen/assets/elements/staircases/straight.py new file mode 100644 index 000000000..820a87cbf --- /dev/null +++ b/infinigen/assets/elements/staircases/straight.py @@ -0,0 +1,592 @@ +# Copyright (c) Princeton University. + +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 + + + +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_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) + 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 From e018308f4d18716e8c84ddfe7395021e7c32951d Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 624/727] Add 5 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/elements/staircases/straight.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/elements/staircases/straight.py b/infinigen/assets/elements/staircases/straight.py index 820a87cbf..99bea84cc 100644 --- a/infinigen/assets/elements/staircases/straight.py +++ b/infinigen/assets/elements/staircases/straight.py @@ -32,6 +32,7 @@ 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): @@ -493,6 +494,10 @@ def decorate_line(self, line, low, high): 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) From 204ae42b845b45fcbeca77e47df5637b5235ae77 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 625/727] Add 4 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/staircases/straight.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/staircases/straight.py b/infinigen/assets/elements/staircases/straight.py index 99bea84cc..bc1b197d5 100644 --- a/infinigen/assets/elements/staircases/straight.py +++ b/infinigen/assets/elements/staircases/straight.py @@ -1,4 +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. + import bpy import bmesh @@ -112,6 +115,7 @@ def __init__(self, factory_seed, coarse=False): 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) From 332cefc2d5641e8c71a74b4a64e3685cd1633472 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 626/727] Add 3 lines to infinigen/assets/elements/staircases/straight.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/staircases/straight.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/elements/staircases/straight.py b/infinigen/assets/elements/staircases/straight.py index bc1b197d5..11a204524 100644 --- a/infinigen/assets/elements/staircases/straight.py +++ b/infinigen/assets/elements/staircases/straight.py @@ -2,6 +2,9 @@ # This 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 From bc26f827f59bc5dfb63b5b0edbb229d752d3dae4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 627/727] Add 60 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/curved.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 infinigen/assets/elements/staircases/curved.py diff --git a/infinigen/assets/elements/staircases/curved.py b/infinigen/assets/elements/staircases/curved.py new file mode 100644 index 000000000..02df46c0c --- /dev/null +++ b/infinigen/assets/elements/staircases/curved.py @@ -0,0 +1,60 @@ +# Copyright (c) Princeton University. + +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.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 From 6404477b894edfdfa9822f286fbb6c9039261e46 Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 628/727] Add 4 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/staircases/curved.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/staircases/curved.py b/infinigen/assets/elements/staircases/curved.py index 02df46c0c..12c61b6a1 100644 --- a/infinigen/assets/elements/staircases/curved.py +++ b/infinigen/assets/elements/staircases/curved.py @@ -1,4 +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. + import numpy as np @@ -25,6 +28,7 @@ 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) From caae08686ae43dc92124c8af6c17bd422ffe42b2 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 629/727] Add 3 lines to infinigen/assets/elements/staircases/curved.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/staircases/curved.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/elements/staircases/curved.py b/infinigen/assets/elements/staircases/curved.py index 12c61b6a1..2190e1af3 100644 --- a/infinigen/assets/elements/staircases/curved.py +++ b/infinigen/assets/elements/staircases/curved.py @@ -2,6 +2,9 @@ # This 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 2f0fa89f41708c760f81d6d6c20a97a6e7e6adcb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 630/727] Add 55 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/elements/staircases/spiral.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 infinigen/assets/elements/staircases/spiral.py diff --git a/infinigen/assets/elements/staircases/spiral.py b/infinigen/assets/elements/staircases/spiral.py new file mode 100644 index 000000000..5e746e969 --- /dev/null +++ b/infinigen/assets/elements/staircases/spiral.py @@ -0,0 +1,55 @@ +# Copyright (c) Princeton University. + +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.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 From 4974e9f3dcd2dad550dd143eb829afdc4a5efdcd Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 631/727] Add 4 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/staircases/spiral.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/staircases/spiral.py b/infinigen/assets/elements/staircases/spiral.py index 5e746e969..b81a7c488 100644 --- a/infinigen/assets/elements/staircases/spiral.py +++ b/infinigen/assets/elements/staircases/spiral.py @@ -1,4 +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. + import numpy as np from numpy.random import uniform @@ -28,6 +31,7 @@ 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 From 7af08e08f10095f6a61779b84e90840dbded4578 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 632/727] Add 3 lines to infinigen/assets/elements/staircases/spiral.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/staircases/spiral.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/elements/staircases/spiral.py b/infinigen/assets/elements/staircases/spiral.py index b81a7c488..6d5a75c9d 100644 --- a/infinigen/assets/elements/staircases/spiral.py +++ b/infinigen/assets/elements/staircases/spiral.py @@ -2,6 +2,9 @@ # This 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 03f3cdf641301b6a0f61aa8005dacc137e327bf8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 633/727] Add 33 lines to infinigen/assets/elements/doors/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/__init__.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 infinigen/assets/elements/doors/__init__.py 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) From 1c674cec4b2ebbf7807230bfdcea615a2add1d1d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 634/727] Add 80 lines to infinigen/assets/elements/doors/louver.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/louver.py | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 infinigen/assets/elements/doors/louver.py 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 From 4261ae32239249634761798cd8644f19f3f0c66b Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 635/727] Add 55 lines to infinigen/assets/elements/doors/lite.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/lite.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 infinigen/assets/elements/doors/lite.py 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 From 5afd3448dd2b69291f6be6d8d5e87b9968f7933e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 636/727] Add 92 lines to infinigen/assets/elements/doors/panel.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/panel.py | 92 ++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 infinigen/assets/elements/doors/panel.py 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 From b44d9bcf3eb8f43418f5a227cdd8b91bb1450867 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 637/727] Add 208 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/base.py | 208 ++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 infinigen/assets/elements/doors/base.py diff --git a/infinigen/assets/elements/doors/base.py b/infinigen/assets/elements/doors/base.py new file mode 100644 index 000000000..443fc5f91 --- /dev/null +++ b/infinigen/assets/elements/doors/base.py @@ -0,0 +1,208 @@ +# Copyright (c) Princeton University. + +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 + +class BaseDoorFactory(AssetFactory): + + def __init__(self, factory_seed, coarse=False): + super(BaseDoorFactory, self).__init__(factory_seed, coarse) + with FixedSeed(self.factory_seed): + 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 From 8e246b0423c01e4902b7a145096784110bfdbb8c Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 638/727] Add 5 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/doors/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/elements/doors/base.py b/infinigen/assets/elements/doors/base.py index 443fc5f91..1d1ea11a7 100644 --- a/infinigen/assets/elements/doors/base.py +++ b/infinigen/assets/elements/doors/base.py @@ -1,4 +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. + import bpy import gin @@ -25,6 +28,8 @@ 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) From 9241cc2a4b61643491987b47e5105c7271512027 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:32 -0700 Subject: [PATCH 639/727] Add 4 lines to infinigen/assets/elements/doors/base.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/elements/doors/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/doors/base.py b/infinigen/assets/elements/doors/base.py index 1d1ea11a7..dd44e0e8e 100644 --- a/infinigen/assets/elements/doors/base.py +++ b/infinigen/assets/elements/doors/base.py @@ -2,6 +2,8 @@ # This 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 @@ -23,6 +25,8 @@ 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): From 3fa57d23927c2e845c8480eca4203bb14e4a1de5 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 640/727] Add 58 lines to infinigen/assets/elements/doors/casing.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/doors/casing.py | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 infinigen/assets/elements/doors/casing.py diff --git a/infinigen/assets/elements/doors/casing.py b/infinigen/assets/elements/doors/casing.py new file mode 100644 index 000000000..f806b5f00 --- /dev/null +++ b/infinigen/assets/elements/doors/casing.py @@ -0,0 +1,58 @@ +# Copyright (c) Princeton University. + +# 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.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) From 5babe5eadab40461c9497d3ddf853c8ed7c7257f Mon Sep 17 00:00:00 2001 From: Karhan Kaan Kayan Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 641/727] Add 4 lines to infinigen/assets/elements/doors/casing.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. --- infinigen/assets/elements/doors/casing.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/elements/doors/casing.py b/infinigen/assets/elements/doors/casing.py index f806b5f00..32317fe93 100644 --- a/infinigen/assets/elements/doors/casing.py +++ b/infinigen/assets/elements/doors/casing.py @@ -1,6 +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 + import bpy import numpy as np from numpy.random import uniform @@ -20,6 +23,7 @@ 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]) From 58ecfb2dbeda4f4f4442208030490a0a936da514 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 642/727] Add 80 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- .../nature_shelf_trinkets/generate.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 infinigen/assets/elements/nature_shelf_trinkets/generate.py 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..8625616d8 --- /dev/null +++ b/infinigen/assets/elements/nature_shelf_trinkets/generate.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. + + +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()) + + + 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 + + + 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)] + asset.location = new_location + butil.apply_transform(asset,loc=True) + return asset \ No newline at end of file From 754841099e1bd253562f143281e9c35a234f23bc Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 643/727] Add 16 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../elements/nature_shelf_trinkets/generate.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/infinigen/assets/elements/nature_shelf_trinkets/generate.py b/infinigen/assets/elements/nature_shelf_trinkets/generate.py index 8625616d8..1fa09fbf6 100644 --- a/infinigen/assets/elements/nature_shelf_trinkets/generate.py +++ b/infinigen/assets/elements/nature_shelf_trinkets/generate.py @@ -1,6 +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: Stamatis Alexandropulos import colorsys @@ -43,6 +44,14 @@ def __init__(self, factory_seed, coarse=False): 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) @@ -51,8 +60,15 @@ def create_placeholder(self, **params) -> bpy.types.Object: return placeholder + 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) From ba788a7ed7061d9eea19360232cadd56ab1f0059 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 644/727] Add 1 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/elements/nature_shelf_trinkets/generate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/elements/nature_shelf_trinkets/generate.py b/infinigen/assets/elements/nature_shelf_trinkets/generate.py index 1fa09fbf6..9566bf86e 100644 --- a/infinigen/assets/elements/nature_shelf_trinkets/generate.py +++ b/infinigen/assets/elements/nature_shelf_trinkets/generate.py @@ -91,6 +91,7 @@ def create_placeholder(self, **params) -> bpy.types.Object: 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 From 4f8dd0fd1d5b8ad30376d6f3d370cadb1088875f Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 645/727] Add 1 lines to infinigen/assets/elements/nature_shelf_trinkets/generate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/elements/nature_shelf_trinkets/generate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/elements/nature_shelf_trinkets/generate.py b/infinigen/assets/elements/nature_shelf_trinkets/generate.py index 9566bf86e..a7a2bc717 100644 --- a/infinigen/assets/elements/nature_shelf_trinkets/generate.py +++ b/infinigen/assets/elements/nature_shelf_trinkets/generate.py @@ -60,6 +60,7 @@ def create_placeholder(self, **params) -> bpy.types.Object: return placeholder + def create_asset(self, i, placeholder=None, **params): asset = self.base_factory.spawn_asset( np.random.randint(1e7), distance=200, From 11d68bb701698f89e88ab968f97b3768c0b87f5a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 646/727] Add 69 lines to infinigen/assets/scatters/clothes.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/scatters/clothes.py | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 infinigen/assets/scatters/clothes.py diff --git a/infinigen/assets/scatters/clothes.py b/infinigen/assets/scatters/clothes.py new file mode 100644 index 000000000..cdd202259 --- /dev/null +++ b/infinigen/assets/scatters/clothes.py @@ -0,0 +1,69 @@ +# Copyright (c) Princeton University. +# This 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: + from infinigen.assets.clothes import blanket, pants, shirt + factory_fn = np.random.choice( + [blanket.BlanketFactory, shirt.ShirtFactory, pants.PantsFactory], + p=probs / probs.sum() + ) + self.factory = factory_fn(np.random.randint(1e5)) + 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): From f11da1af5510a9f78043772bfdadb3cb84adc82c Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 647/727] Add 9 lines to infinigen/assets/scatters/clothes.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/scatters/clothes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/assets/scatters/clothes.py b/infinigen/assets/scatters/clothes.py index cdd202259..123197206 100644 --- a/infinigen/assets/scatters/clothes.py +++ b/infinigen/assets/scatters/clothes.py @@ -2,6 +2,7 @@ # This 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 @@ -41,12 +42,19 @@ def cloth_sim(clothes, obj=None, end_frame=50, **kwargs): 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 @@ -67,3 +75,4 @@ def apply(self, obj, selection=None, **kwargs): def apply(obj, selection=None, **kwargs): + ClothesCover().apply(obj, selection, **kwargs) \ No newline at end of file From 9f467ec4cd2f6815153963b93b79424a5c2cf571 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 648/727] Add 232 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- .../assets/lighting/ceiling_classic_lamp.py | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 infinigen/assets/lighting/ceiling_classic_lamp.py diff --git a/infinigen/assets/lighting/ceiling_classic_lamp.py b/infinigen/assets/lighting/ceiling_classic_lamp.py new file mode 100644 index 000000000..52f21161a --- /dev/null +++ b/infinigen/assets/lighting/ceiling_classic_lamp.py @@ -0,0 +1,232 @@ +# Copyright (c) Princeton University. +# This 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 + + + +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, + + 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}) + + + + + + def __init__(self, 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, **_): + return obj From cc15723ee3efbeefb818a8fe00dcf39d358f0ec1 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 649/727] Add 5 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/lighting/ceiling_classic_lamp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/lighting/ceiling_classic_lamp.py b/infinigen/assets/lighting/ceiling_classic_lamp.py index 52f21161a..e2861878b 100644 --- a/infinigen/assets/lighting/ceiling_classic_lamp.py +++ b/infinigen/assets/lighting/ceiling_classic_lamp.py @@ -19,6 +19,7 @@ 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): @@ -191,6 +192,7 @@ def geometry_nodes(nw: NodeWrangler): 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]}) @@ -229,4 +231,7 @@ def create_placeholder(self, **_): 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 From 531c19a9ecdffc8e65ba8bdce7dc2826d14f6872 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 650/727] Add 2 lines to infinigen/assets/lighting/ceiling_classic_lamp.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/lighting/ceiling_classic_lamp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/lighting/ceiling_classic_lamp.py b/infinigen/assets/lighting/ceiling_classic_lamp.py index e2861878b..60307e7be 100644 --- a/infinigen/assets/lighting/ceiling_classic_lamp.py +++ b/infinigen/assets/lighting/ceiling_classic_lamp.py @@ -204,7 +204,9 @@ def geometry_nodes(nw: NodeWrangler): +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), From 9c09c375b1fb52098bfa8d3ac1539f5fee97ed62 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 651/727] Add 56 lines to infinigen/assets/lighting/indoor_lights.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/lighting/indoor_lights.py | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 infinigen/assets/lighting/indoor_lights.py 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 From d9bbc2f80c148c14f770623c7f0ee9aa7fb38695 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 652/727] Add 318 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/lighting/lamp.py | 318 ++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 infinigen/assets/lighting/lamp.py diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py new file mode 100644 index 000000000..3476120d2 --- /dev/null +++ b/infinigen/assets/lighting/lamp.py @@ -0,0 +1,318 @@ +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 + + def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], lamp_type="FloorLamp"): + super(LampFactory, self).__init__(factory_seed, coarse=coarse) + + 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) + 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 + 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 + obj = butil.spawn_cube() + + +@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 + 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, + 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, + 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, + 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), + 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), + multiply = nw.new_node(Nodes.Math, + input_kwargs={0: group_input.outputs["TopRadius"], 1: 0.8000}, + attrs={'operation': 'MULTIPLY'}) + 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, + 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, + 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)), + 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, + 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, + 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}) + From 4a246e9715d6b0818fb9e3b189593a3e1cefe93a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 653/727] Add 114 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/lighting/lamp.py | 114 ++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py index 3476120d2..3aa6b698d 100644 --- a/infinigen/assets/lighting/lamp.py +++ b/infinigen/assets/lighting/lamp.py @@ -12,6 +12,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.core.placement.factory import AssetFactory +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) @@ -31,6 +32,7 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], lamp_typ "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, @@ -81,11 +83,13 @@ def sample_parameters(self, dimensions, use_default=False): 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 + params = { "StandRadius": stand_radius, "BaseRadius": base_radius, @@ -100,45 +104,77 @@ def sample_parameters(self, dimensions, use_default=False): "CurvePoint3": (x3, 0.0, z3) } return params + obj = butil.spawn_cube() + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) @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 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, + 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, + 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}) @@ -154,43 +190,68 @@ def nodegroup_bulb_rack(nw: NodeWrangler): ('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') @@ -201,15 +262,23 @@ def nodegroup_reversiable_bulb(nw: NodeWrangler): expose_input=[('NodeSocketFloat', 'Scale', 0.3000), 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}) @@ -227,37 +296,59 @@ def nodegroup_lamp_head(nw: NodeWrangler): multiply = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["TopRadius"], 1: 0.8000}, attrs={'operation': 'MULTIPLY'}) + 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, + 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, + 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') @@ -276,42 +367,65 @@ def nodegroup_lamp_geometry(nw: NodeWrangler): ('NodeSocketVectorTranslation', 'CurvePoint1', (0.0000, 0.0000, 0.0000)), ('NodeSocketVectorTranslation', 'CurvePoint2', (0.0000, 0.0000, 0.0000)), 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, + 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, 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}) From dc72b194557c29eb324e94553030cc6749da1428 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 654/727] Add 75 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/lighting/lamp.py | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py index 3aa6b698d..c20b4edf7 100644 --- a/infinigen/assets/lighting/lamp.py +++ b/infinigen/assets/lighting/lamp.py @@ -11,6 +11,7 @@ from infinigen.core.util.math import FixedSeed from infinigen.core.placement.factory import AssetFactory +from infinigen.assets.material_assignments import AssetList class LampFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], lamp_type="FloorLamp"): @@ -63,6 +64,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.], lamp_typ }} 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": @@ -109,13 +138,20 @@ def sample_parameters(self, dimensions, use_default=False): 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_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}) @@ -152,6 +188,7 @@ def nodegroup_bulb(nw: NodeWrangler): 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) @@ -170,6 +207,7 @@ def nodegroup_bulb(nw: NodeWrangler): 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]}) @@ -260,6 +298,14 @@ def nodegroup_reversiable_bulb(nw: NodeWrangler): 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"]}) @@ -293,10 +339,21 @@ def nodegroup_lamp_head(nw: NodeWrangler): ('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, @@ -311,6 +368,7 @@ def nodegroup_lamp_head(nw: NodeWrangler): 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}) @@ -345,6 +403,7 @@ def nodegroup_lamp_head(nw: NodeWrangler): 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]}) @@ -366,6 +425,11 @@ def nodegroup_lamp_geometry(nw: NodeWrangler): ('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}) @@ -392,6 +456,7 @@ def nodegroup_lamp_geometry(nw: NodeWrangler): 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}, @@ -406,6 +471,16 @@ def nodegroup_lamp_geometry(nw: NodeWrangler): 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}) From 184632556387b6f01c042517962c12ed4931c09a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 655/727] Add 32 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/lighting/lamp.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py index c20b4edf7..1134a1c12 100644 --- a/infinigen/assets/lighting/lamp.py +++ b/infinigen/assets/lighting/lamp.py @@ -1,3 +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: primary author +# - Alexander Raistrick: add point light + import bpy import random import mathutils @@ -11,12 +19,17 @@ 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 = { @@ -134,7 +147,14 @@ def sample_parameters(self, dimensions, use_default=False): } 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) + return obj def finalize_assets(self, assets): @@ -143,6 +163,18 @@ def finalize_assets(self, 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): From 5656f76f439a8e2667eeef080031ad60d82e1be8 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 656/727] Add 9 lines to infinigen/assets/lighting/lamp.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/lighting/lamp.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infinigen/assets/lighting/lamp.py b/infinigen/assets/lighting/lamp.py index 1134a1c12..f78caded8 100644 --- a/infinigen/assets/lighting/lamp.py +++ b/infinigen/assets/lighting/lamp.py @@ -131,6 +131,11 @@ def sample_parameters(self, dimensions, use_default=False): 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, @@ -154,7 +159,11 @@ def create_asset(self, i, **params): 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): From 4d44aee450de6e7d4306e761eee5446b647ffadb Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 657/727] Add 27 lines to infinigen/assets/lighting/three_point_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/lighting/three_point_lighting.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 infinigen/assets/lighting/three_point_lighting.py diff --git a/infinigen/assets/lighting/three_point_lighting.py b/infinigen/assets/lighting/three_point_lighting.py new file mode 100644 index 000000000..bde95cd1a --- /dev/null +++ b/infinigen/assets/lighting/three_point_lighting.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: 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 From cf57f6f861333f7a98d9aeee1102a0f4e5df67b7 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 658/727] Add 1 lines to infinigen/assets/lighting/three_point_lighting.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/lighting/three_point_lighting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/lighting/three_point_lighting.py b/infinigen/assets/lighting/three_point_lighting.py index bde95cd1a..e1dba2e16 100644 --- a/infinigen/assets/lighting/three_point_lighting.py +++ b/infinigen/assets/lighting/three_point_lighting.py @@ -2,6 +2,7 @@ # This 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 74c42ee19f8dd36fb38bea5d7e7106fcd8e3843b Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 659/727] Add 145 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/lighting/ceiling_lights.py | 145 ++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 infinigen/assets/lighting/ceiling_lights.py diff --git a/infinigen/assets/lighting/ceiling_lights.py b/infinigen/assets/lighting/ceiling_lights.py new file mode 100644 index 000000000..5bc263be4 --- /dev/null +++ b/infinigen/assets/lighting/ceiling_lights.py @@ -0,0 +1,145 @@ +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 import surface +from infinigen.core.util import blender as butil + +from infinigen.core.placement.factory import AssetFactory + +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.params = self.sample_parameters(dimensions) + + 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: + Thickness = U(0.005, 0.05) + Curvature = U(0.1, 0.5) + params = { + "Radius": Radius, + "Thickness": Thickness, + "InnerRadius": InnerRadius, + "Height": Height, + "InnerHeight": InnerHeight, + "Curvature": Curvature, + } + return params + obj = butil.spawn_cube() + + + +@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), + + 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, + + 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, + + 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 From 643889554f554e4786db92b48872f010385d6161 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 660/727] Add 35 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/lighting/ceiling_lights.py | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/infinigen/assets/lighting/ceiling_lights.py b/infinigen/assets/lighting/ceiling_lights.py index 5bc263be4..6996f0513 100644 --- a/infinigen/assets/lighting/ceiling_lights.py +++ b/infinigen/assets/lighting/ceiling_lights.py @@ -1,3 +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: +# - +# - Alexander Raistrick: add point light + import bpy import random import mathutils @@ -5,11 +13,17 @@ 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 + + class CeilingLightFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): super(CeilingLightFactory, self).__init__(factory_seed, coarse=coarse) @@ -38,13 +52,19 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): "Curvature": 0.4, }] with FixedSeed(factory_seed): + self.light_factory = PointLampFactory(factory_seed) self.params = self.sample_parameters(dimensions) + self.beveler = BevelSharp(mult=U(1, 3)) 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, @@ -55,7 +75,22 @@ def sample_parameters(self, dimensions, use_default=False): "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 From 850d882baa06459e78eb3ab59a04656cb1a11dad Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 661/727] Add 34 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/lighting/ceiling_lights.py | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/infinigen/assets/lighting/ceiling_lights.py b/infinigen/assets/lighting/ceiling_lights.py index 6996f0513..7dad3944f 100644 --- a/infinigen/assets/lighting/ceiling_lights.py +++ b/infinigen/assets/lighting/ceiling_lights.py @@ -22,6 +22,7 @@ from .indoor_lights import PointLampFactory from infinigen.assets.utils.autobevel import BevelSharp +from infinigen.assets.material_assignments import AssetList class CeilingLightFactory(AssetFactory): @@ -54,8 +55,34 @@ def __init__(self, factory_seed, coarse=False, dimensions=[1., 1., 1.]): 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))] @@ -92,6 +119,8 @@ def create_asset(self, i, placeholder, **params): return obj + if self.scratch: + if self.edge_wear: @node_utils.to_nodegroup('nodegroup_ceiling_light_geometry', singleton=True, type='GeometryNodeTree') @@ -104,6 +133,9 @@ def nodegroup_ceiling_light_geometry(nw: NodeWrangler): ('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'}) @@ -129,6 +161,7 @@ def nodegroup_ceiling_light_geometry(nw: NodeWrangler): 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}) @@ -167,6 +200,7 @@ def nodegroup_ceiling_light_geometry(nw: NodeWrangler): 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]}) From 8fc7a0817cf3e28262e26ae6eb096743d9e6d581 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 662/727] Add 3 lines to infinigen/assets/lighting/ceiling_lights.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/lighting/ceiling_lights.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/lighting/ceiling_lights.py b/infinigen/assets/lighting/ceiling_lights.py index 7dad3944f..ccf31fa34 100644 --- a/infinigen/assets/lighting/ceiling_lights.py +++ b/infinigen/assets/lighting/ceiling_lights.py @@ -119,8 +119,11 @@ def create_asset(self, i, placeholder, **params): 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') From 5b63fce713fc41f82c5b75f1c82822cbd1ab89a3 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 663/727] Add 33 lines to infinigen/assets/lighting/hdri_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/lighting/hdri_lighting.py | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 infinigen/assets/lighting/hdri_lighting.py 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}) From fd7c02a05bd41048aaaea1d4a417688facb30be8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 664/727] Add 34 lines to infinigen/assets/lighting/holdout_lighting.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/lighting/holdout_lighting.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 infinigen/assets/lighting/holdout_lighting.py 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 From 8adc1a324c2d6f758dd03af9dd2445d5ff95fc27 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 665/727] Add 123 lines to infinigen/assets/seating/mattress.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/mattress.py | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 infinigen/assets/seating/mattress.py diff --git a/infinigen/assets/seating/mattress.py b/infinigen/assets/seating/mattress.py new file mode 100644 index 000000000..104008463 --- /dev/null +++ b/infinigen/assets/seating/mattress.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 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 + + + +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) + + 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) From a3f4e8af2a42a7e65ce4d3e39011cd72aa06d401 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:33 -0700 Subject: [PATCH 666/727] Add 3 lines to infinigen/assets/seating/mattress.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/mattress.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/seating/mattress.py b/infinigen/assets/seating/mattress.py index 104008463..1d9a9612a 100644 --- a/infinigen/assets/seating/mattress.py +++ b/infinigen/assets/seating/mattress.py @@ -21,6 +21,7 @@ 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 @@ -79,6 +80,8 @@ def __init__(self, factory_seed, coarse=False): 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( From de8cb04933fdf05db30799ce84259b9f3f91da9a Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 667/727] Add 112 lines to infinigen/assets/seating/pillow.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/pillow.py | 112 +++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 infinigen/assets/seating/pillow.py diff --git a/infinigen/assets/seating/pillow.py b/infinigen/assets/seating/pillow.py new file mode 100644 index 000000000..249be2336 --- /dev/null +++ b/infinigen/assets/seating/pillow.py @@ -0,0 +1,112 @@ +# Copyright (c) Princeton University. +# This 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 + + +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) + + + 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) From 804aec66405ea6b4efad233461f2d3c96070a481 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 668/727] Add 5 lines to infinigen/assets/seating/pillow.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/pillow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/seating/pillow.py b/infinigen/assets/seating/pillow.py index 249be2336..f5f871a24 100644 --- a/infinigen/assets/seating/pillow.py +++ b/infinigen/assets/seating/pillow.py @@ -15,6 +15,7 @@ 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): @@ -36,6 +37,10 @@ def __init__(self, factory_seed, coarse=False): 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: From 06d0e21f2a13823640550626f382d6dd8ce2c44d Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 669/727] Add 9 lines to infinigen/assets/seating/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 infinigen/assets/seating/__init__.py diff --git a/infinigen/assets/seating/__init__.py b/infinigen/assets/seating/__init__.py new file mode 100644 index 000000000..785137eb9 --- /dev/null +++ b/infinigen/assets/seating/__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 .bedframe import BedFrameFactory +from .pillow import PillowFactory +from .mattress import MattressFactory +from .bed import BedFactory +from .chairs import * From d4b97878d3ab2e04a191c12ff0aa95851a3110b0 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 670/727] Add 2 lines to infinigen/assets/seating/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/seating/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/seating/__init__.py b/infinigen/assets/seating/__init__.py index 785137eb9..5abb955c1 100644 --- a/infinigen/assets/seating/__init__.py +++ b/infinigen/assets/seating/__init__.py @@ -2,6 +2,8 @@ # This 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 6202247f6106012e2cd35c84bd0b06e5dcd033e9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 671/727] Add 186 lines to infinigen/assets/seating/bed.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/bed.py | 186 ++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 infinigen/assets/seating/bed.py 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) From 3b4628c9626dca8c2eca559c50c039cc2e6205d8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 672/727] Add 172 lines to infinigen/assets/seating/bedframe.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/bedframe.py | 172 +++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 infinigen/assets/seating/bedframe.py diff --git a/infinigen/assets/seating/bedframe.py b/infinigen/assets/seating/bedframe.py new file mode 100644 index 000000000..d1a4bb5eb --- /dev/null +++ b/infinigen/assets/seating/bedframe.py @@ -0,0 +1,172 @@ +# Copyright (c) Princeton University. +# This 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 + + +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 + + + 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 From 51063eb7fd8d67b3cddeb9a941bb31c2c3d01058 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 673/727] Add 7 lines to infinigen/assets/seating/bedframe.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/bedframe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infinigen/assets/seating/bedframe.py b/infinigen/assets/seating/bedframe.py index d1a4bb5eb..d7029a2b3 100644 --- a/infinigen/assets/seating/bedframe.py +++ b/infinigen/assets/seating/bedframe.py @@ -19,6 +19,7 @@ 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): @@ -48,7 +49,13 @@ def __init__(self, factory_seed, coarse=False): 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 From 6637750d6ff7d440384b04381bfb64f70d116cc1 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 674/727] Add 405 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/seating/sofa.py | 405 +++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 infinigen/assets/seating/sofa.py diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py new file mode 100644 index 000000000..7d26dcf58 --- /dev/null +++ b/infinigen/assets/seating/sofa.py @@ -0,0 +1,405 @@ + +import random + + group_input = nw.new_node(Nodes.GroupInput, + ('NodeSocketVector', 'Dimensions', (0.0000, 0.9000, 2.5000)), + ('NodeSocketFloat', 'Baseboard Height', 0.1300), + ('NodeSocketFloat', 'Backrest Width', 0.1100), + ('NodeSocketFloat', 'Backrest Angle', -0.2000), + ('NodeSocketFloatFactor', 'arm_width', 0.7000), + ('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), + input_kwargs={0: group_input.outputs["Dimensions"], 1: (0.0000, 0.5000, 0.0000)}, + reroute = nw.new_node(Nodes.Reroute, input_kwargs={'Input': group_input.outputs["Arm Dimensions"]}) + input_kwargs={'Location': multiply.outputs["Vector"], 'CenteringLoc': (0.0000, 1.0000, 0.0000), 'Dimensions': reroute, 'Vertices Z': 10}, + reroute_1 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': arm_cube}) + 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"]}) + input_kwargs={0: separate_xyz.outputs["Y"], 1: separate_xyz_2.outputs["Y"]}, + 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)}) + input_kwargs={0: separate_xyz_14.outputs["Z"], 1: separate_xyz_15.outputs["Z"]}, + 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}) + input_kwargs={'Location': multiply_3.outputs["Vector"], 'CenteringLoc': (0.0000, 1.0000, 0.0000), 'Dimensions': reroute_2}, + 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}) + input_kwargs={'Side Segments': 4, 'Radius': separate_xyz_4.outputs["Y"], 'Depth': reroute_3}, + 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"]}) + 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"]}) + + input_kwargs={0: separate_xyz_7.outputs["Y"], 1: -2.0000, 2: separate_xyz_8.outputs["Y"]}, + + 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"]}) + + input_kwargs={'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': combine_xyz_3, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2}, + 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)}, + + input_kwargs={0: group_input.outputs["Arm Dimensions"], 1: (0.0000, -2.0000, 0.0000), 2: group_input.outputs["Dimensions"]}, + + input_kwargs={0: group_input.outputs["Back Dimensions"], 1: (-1.0000, 0.0000, 0.0000), 2: multiply_add_1.outputs["Vector"]}, + + 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"]}) + + 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}, + 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)}, + multiply_12 = nw.new_node(Nodes.VectorMath, + input_kwargs={0: group_input.outputs["Dimensions"], 1: (1.0000, 1.0000, 0.0000)}, + 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"]}, + + 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}) + + 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)}, + 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)}, + input_kwargs={'CenteringLoc': (0.0000, 0.5000, 0.0000), 'Dimensions': multiply_18.outputs["Vector"], 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2}, + + 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"]}, + + combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, + input_kwargs={'X': subtract_7, 'Y': divide_3, 'Z': group_input.outputs["Backrest Width"]}) + input_kwargs={'CenteringLoc': (0.1000, 0.5000, 1.0000), 'Dimensions': combine_xyz_8, 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2}, + store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute, + multiply_19 = nw.new_node(Nodes.Math, + input_kwargs={0: group_input.outputs["Backrest Width"], 1: -1.0000}, + 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]}) + switch_1 = nw.new_node(Nodes.Switch, input_kwargs={1: True, 14: join_geometry_7, 15: subdivision_surface_2}) + input_kwargs={'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': group_input.outputs["Dimensions"], 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2}, + reroute_7 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': bounding_box}) + reroute_8 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': reroute_7}) + input_kwargs={'Geometry': switch_1.outputs[6], 'BoundingBox': reroute_8}, + + + uniform(0.06, 0.15), + uniform(0.7, 1), + 'Baseboard Height': uniform(0.05, 0.09), + 'arm_width': uniform(0.6, 0.9), + 'Arm_height': uniform(0.7,1.0), + 'arms_angle': uniform(0.0, 1.08), + 'Count': 1 if uniform()>0.2 else 4, + '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) + + def __init__(self, factory_seed): + from infinigen.assets.clothes import blanket + super().__init__(factory_seed) + with FixedSeed(factory_seed): + self.params = sofa_parameter_distribution() + + def create_placeholder(self, **_): + obj = butil.spawn_vert() + butil.modify_mesh( + obj, + 'NODES', + node_group=nodegroup_sofa_geometry(), + apply=True + ) + tagging.tag_system.relabel_obj(obj) + 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) + From 5c016c408c76c0f1bd97b449f1a149a7a8050b3e Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 675/727] Add 239 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/seating/sofa.py | 239 +++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py index 7d26dcf58..d3a89bd8a 100644 --- a/infinigen/assets/seating/sofa.py +++ b/infinigen/assets/seating/sofa.py @@ -1,12 +1,95 @@ +# Authors: Alexander Raistrick, Stamatis Alexandropolous, Yiming Zuo +import bpy +import bpy +import mathutils import random +import numpy as np + +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.util.random import log_uniform, clip_gaussian + + +@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'}) + + +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): + 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), @@ -16,9 +99,17 @@ ('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}) separate_xyz = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': position}) separate_xyz_1 = nw.new_node(Nodes.SeparateXYZ, input_kwargs={'Vector': reroute}) @@ -62,41 +153,83 @@ reroute_2 = nw.new_node(Nodes.Reroute, input_kwargs={'Input': combine_xyz_1}) 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}, + arm_cylinder = nw.new_node(Nodes.StoreNamedAttribute, + input_kwargs={'Geometry': arm_cylinder.outputs["Mesh"], 'Name': 'UVMap', 3: arm_cylinder.outputs["UV Map"]}, 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)}) + + 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'}) @@ -226,16 +359,22 @@ 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"]}) @@ -243,7 +382,10 @@ 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, @@ -251,16 +393,22 @@ 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, @@ -276,8 +424,12 @@ multiply_18 = nw.new_node(Nodes.VectorMath, input_kwargs={0: combine_xyz_5, 1: (1.0000, 1.0300, 1.0000)}, + + 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'}) 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'}) @@ -329,22 +481,34 @@ 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') + + store_named_attribute_3 = nw.new_node(Nodes.StoreNamedAttribute, multiply_19 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Backrest Width"], 1: -1.0000}, @@ -354,39 +518,95 @@ 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 + }) + input_kwargs={'CenteringLoc': (0.0000, 0.5000, -1.0000), 'Dimensions': group_input.outputs["Dimensions"], 'Vertices X': 2, 'Vertices Y': 2, 'Vertices Z': 2}, + 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), + 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 + ), 'Baseboard Height': uniform(0.05, 0.09), + 'Seat Margin': uniform(0.9700, 1), + + '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() def create_placeholder(self, **_): obj = butil.spawn_vert() @@ -394,12 +614,31 @@ def create_placeholder(self, **_): obj, 'NODES', node_group=nodegroup_sofa_geometry(), + ng_inputs={**self.params, }, apply=True ) tagging.tag_system.relabel_obj(obj) 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 From e8f0f7bb642b9eec582aa71912b324eb2b574539 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 676/727] Add 75 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/seating/sofa.py | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py index d3a89bd8a..dc4e24c2b 100644 --- a/infinigen/assets/seating/sofa.py +++ b/infinigen/assets/seating/sofa.py @@ -6,6 +6,7 @@ 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 @@ -76,6 +77,7 @@ def nodegroup_corner_cube(nw: NodeWrangler): @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), @@ -107,28 +109,51 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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}) @@ -152,22 +177,35 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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( @@ -178,6 +216,7 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): '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), @@ -188,6 +227,8 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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"]}) @@ -401,6 +442,7 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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'}) @@ -414,7 +456,9 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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"]}) @@ -424,12 +468,14 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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'}) + 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'}) @@ -470,6 +516,7 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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}) @@ -508,15 +555,33 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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, @@ -524,11 +589,17 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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'], @@ -536,7 +607,9 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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}) @@ -578,7 +651,9 @@ def sofa_parameter_distribution(dimensions=None): 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], From 5b4fbc42538628f84fb741539a2999692e7ccafa Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 677/727] Add 18 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/sofa.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py index dc4e24c2b..b1dc17e84 100644 --- a/infinigen/assets/seating/sofa.py +++ b/infinigen/assets/seating/sofa.py @@ -1,3 +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: Alexander Raistrick, Stamatis Alexandropolous, Yiming Zuo import bpy @@ -29,19 +32,26 @@ def nodegroup_array_fill_line(nw: NodeWrangler): ('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') @@ -56,20 +66,26 @@ def nodegroup_corner_cube(nw: NodeWrangler): ('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 @@ -639,6 +655,7 @@ def sofa_parameter_distribution(dimensions=None): ), 'Back Dimensions': ( uniform(0.15, 0.25), + 0.0000, uniform(0.5, 0.75) ), 'Seat Dimensions': ( @@ -648,6 +665,7 @@ def sofa_parameter_distribution(dimensions=None): ), 'Foot Dimensions': ( uniform(0.07, 0.25), + 0.06, 0.06 ), 'Baseboard Height': uniform(0.05, 0.09), From 76c6c31feacadf4d6592a8a0f4f2ba72ab9859ff Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 678/727] Add 4 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/sofa.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py index b1dc17e84..3cabef2bd 100644 --- a/infinigen/assets/seating/sofa.py +++ b/infinigen/assets/seating/sofa.py @@ -21,6 +21,7 @@ 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): @@ -700,6 +701,8 @@ def __init__(self, factory_seed): #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() @@ -711,6 +714,7 @@ def create_placeholder(self, **_): 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, **_): From 89cb603f3d4acf5922b31fe74117a03d0216d43a Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 679/727] Add 2 lines to infinigen/assets/seating/sofa.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/seating/sofa.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/seating/sofa.py b/infinigen/assets/seating/sofa.py index 3cabef2bd..0f33fb326 100644 --- a/infinigen/assets/seating/sofa.py +++ b/infinigen/assets/seating/sofa.py @@ -18,6 +18,7 @@ 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 @@ -492,6 +493,7 @@ def nodegroup_sofa_geometry(nw: NodeWrangler): 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) From 4692cfa27d364ba2628fca4b854102325dd9757e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 680/727] Add 140 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../assets/seating/chairs/office_chair.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 infinigen/assets/seating/chairs/office_chair.py diff --git a/infinigen/assets/seating/chairs/office_chair.py b/infinigen/assets/seating/chairs/office_chair.py new file mode 100644 index 000000000..5f8cbfbae --- /dev/null +++ b/infinigen/assets/seating/chairs/office_chair.py @@ -0,0 +1,140 @@ +# Authors: Yiming Zuo + +import bpy +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.math import FixedSeed +from infinigen.core.placement.factory import AssetFactory + + +from infinigen.assets.tables.cocktail_table import geometry_create_legs + +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'], + }) + + 'Translation': (0.0000, 0.0000, kwargs['Top Height'])}) + seat_instance = nw.new_node(Nodes.SetMaterial, + + 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): + @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), + '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) + + (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, + }) + + 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, + '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, + }) + + else: + raise NotImplementedError + + + 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 + + From 1b514b32ca0f724bd9961637e91c47098e0bf3b2 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 681/727] Add 36 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- .../assets/seating/chairs/office_chair.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/infinigen/assets/seating/chairs/office_chair.py b/infinigen/assets/seating/chairs/office_chair.py index 5f8cbfbae..0022a5a96 100644 --- a/infinigen/assets/seating/chairs/office_chair.py +++ b/infinigen/assets/seating/chairs/office_chair.py @@ -10,6 +10,7 @@ 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 @@ -29,6 +30,7 @@ def geometry_assemble_chair(nw: NodeWrangler, **kwargs): '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]}) @@ -41,6 +43,33 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): 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 @@ -71,6 +100,7 @@ def sample_parameters(dimensions): '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, @@ -90,6 +120,7 @@ def sample_parameters(dimensions): '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": @@ -102,6 +133,7 @@ def sample_parameters(dimensions): '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]) }) @@ -125,11 +157,13 @@ def sample_parameters(dimensions): '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): @@ -137,4 +171,6 @@ def create_asset(self, **params): size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) obj = bpy.context.active_object + if self.scratch: + if self.edge_wear: From 7883753bb996eed48d444e0ef989937f7576f67e Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 682/727] Add 18 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../assets/seating/chairs/office_chair.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/infinigen/assets/seating/chairs/office_chair.py b/infinigen/assets/seating/chairs/office_chair.py index 0022a5a96..45cb2e6e6 100644 --- a/infinigen/assets/seating/chairs/office_chair.py +++ b/infinigen/assets/seating/chairs/office_chair.py @@ -1,6 +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: Yiming Zuo import bpy +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 @@ -8,6 +12,7 @@ from infinigen.core.util.math import FixedSeed 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 @@ -26,14 +31,20 @@ def geometry_assemble_chair(nw: NodeWrangler, **kwargs): '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): @@ -73,6 +84,7 @@ def get_material_params(self, leg_style): @staticmethod def sample_parameters(dimensions): # all in meters + if dimensions is None: x = uniform(0.5, 0.6) z = uniform(1.0, 1.4) @@ -114,6 +126,7 @@ def sample_parameters(dimensions): 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({ @@ -171,6 +184,11 @@ def create_asset(self, **params): size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) obj = bpy.context.active_object + return obj + + def finalize_assets(self, assets): if self.scratch: + self.scratch.apply(assets) if self.edge_wear: + self.edge_wear.apply(assets) From 76f9436c50a703b2590ecc0f242606f90cac916b Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 683/727] Add 12 lines to infinigen/assets/seating/chairs/office_chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/seating/chairs/office_chair.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/infinigen/assets/seating/chairs/office_chair.py b/infinigen/assets/seating/chairs/office_chair.py index 45cb2e6e6..7a4f3ba5a 100644 --- a/infinigen/assets/seating/chairs/office_chair.py +++ b/infinigen/assets/seating/chairs/office_chair.py @@ -4,12 +4,17 @@ # 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 @@ -184,6 +189,13 @@ def create_asset(self, **params): 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): From 2f048b4d8ec25a6e5076909572dcb76e0ac1f797 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 684/727] Add 328 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/chairs/chair.py | 328 +++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 infinigen/assets/seating/chairs/chair.py diff --git a/infinigen/assets/seating/chairs/chair.py b/infinigen/assets/seating/chairs/chair.py new file mode 100644 index 000000000..4c477e19a --- /dev/null +++ b/infinigen/assets/seating/chairs/chair.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: 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.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) + + if uniform() < .3: + self.panel_surface = self.surface + else: + self.panel_surface = materials['panel'].assign_material() + 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 + ) + + 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) + 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 + From 552f96a8696ccb00c5e6c1b4f969e90f21b3a06d Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 685/727] Add 22 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/seating/chairs/chair.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/infinigen/assets/seating/chairs/chair.py b/infinigen/assets/seating/chairs/chair.py index 4c477e19a..054a7720f 100644 --- a/infinigen/assets/seating/chairs/chair.py +++ b/infinigen/assets/seating/chairs/chair.py @@ -26,6 +26,7 @@ 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): @@ -75,6 +76,10 @@ def __init__(self, factory_seed, coarse=False): self.panel_surface = self.surface else: self.panel_surface = materials['panel'].assign_material() + #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() @@ -116,21 +121,37 @@ def create_placeholder(self, **kwargs) -> bpy.types.Object: -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): @@ -326,3 +347,4 @@ def solidify(self, obj, axis, thickness=None): write_attribute(obj, 1, 'limb', 'FACE') return obj + From 9f2869c8319a8f7ac0b4d590e05f89ecfdcf3bff Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 686/727] Add 14 lines to infinigen/assets/seating/chairs/chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/chairs/chair.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/infinigen/assets/seating/chairs/chair.py b/infinigen/assets/seating/chairs/chair.py index 054a7720f..ed37dafdf 100644 --- a/infinigen/assets/seating/chairs/chair.py +++ b/infinigen/assets/seating/chairs/chair.py @@ -20,6 +20,7 @@ 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 @@ -72,10 +73,23 @@ def __init__(self, factory_seed, coarse=False): 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), From f8f25322aed33b94cd4a719ffcf307fffc2849b4 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 687/727] Add 7 lines to infinigen/assets/seating/chairs/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/chairs/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 infinigen/assets/seating/chairs/__init__.py 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 From 514f8db7d71bb37a8ffa7ecb8b2cf21b58cf17c6 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 688/727] Add 117 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/seating/chairs/bar_chair.py | 117 +++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 infinigen/assets/seating/chairs/bar_chair.py diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py new file mode 100644 index 000000000..16097f730 --- /dev/null +++ b/infinigen/assets/seating/chairs/bar_chair.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: Yiming Zuo + + +import bpy +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.assets.tables.cocktail_table import geometry_create_legs + +def geometry_assemble_chair(nw: NodeWrangler, **kwargs): + # Code generated using version 2.6.4 of the node_transpiler + '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): + @staticmethod + def sample_parameters(dimensions): + # all in meters + if dimensions is None: + x = uniform(0.35, 0.45) + 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) + + (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, + }) + + 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, + '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, + }) + + else: + raise NotImplementedError + + + + + 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 + + + return obj + From 5991fc2e87d9299da1e733e569a86f281585a050 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 689/727] Add 37 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/chairs/bar_chair.py | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py index 16097f730..a2df72ab2 100644 --- a/infinigen/assets/seating/chairs/bar_chair.py +++ b/infinigen/assets/seating/chairs/bar_chair.py @@ -13,9 +13,13 @@ 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) '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]}) @@ -28,6 +32,35 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): 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 @@ -63,6 +96,7 @@ def sample_parameters(dimensions): '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": @@ -75,6 +109,7 @@ def sample_parameters(dimensions): '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]) }) @@ -98,6 +133,7 @@ def sample_parameters(dimensions): 'Leg Wheel Width': wheel_width, 'Leg Wheel Rot': wheel_rot, 'Leg Pole Length': pole_length, + # 'Leg Material': choice(['metal']) }) else: @@ -105,6 +141,7 @@ def sample_parameters(dimensions): + return parameters, leg_style def create_asset(self, **params): From 974ab1eb8d1ac72fb3e54fe9c846addd492df7e9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 690/727] Add 15 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/seating/chairs/bar_chair.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py index a2df72ab2..9ef024ea3 100644 --- a/infinigen/assets/seating/chairs/bar_chair.py +++ b/infinigen/assets/seating/chairs/bar_chair.py @@ -5,12 +5,14 @@ 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.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 @@ -20,9 +22,15 @@ def geometry_assemble_chair(nw: NodeWrangler, **kwargs): 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): @@ -90,6 +98,7 @@ def sample_parameters(dimensions): 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({ @@ -152,3 +161,9 @@ def create_asset(self, **params): return obj + def finalize_assets(self, assets): + if self.scratch: + self.scratch.apply(assets) + if self.edge_wear: + self.edge_wear.apply(assets) + From ebca9d32cdf8c0c7031cc263a1cd42383d88a6ce Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:34 -0700 Subject: [PATCH 691/727] Add 3 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/seating/chairs/bar_chair.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py index 9ef024ea3..a57666d66 100644 --- a/infinigen/assets/seating/chairs/bar_chair.py +++ b/infinigen/assets/seating/chairs/bar_chair.py @@ -74,6 +74,7 @@ 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 @@ -158,6 +159,8 @@ def create_asset(self, **params): 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 From c20e7d28e361f2ad8e78cf0b17c098c4e93458bf Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 692/727] Add 1 lines to infinigen/assets/seating/chairs/bar_chair.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/seating/chairs/bar_chair.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/seating/chairs/bar_chair.py b/infinigen/assets/seating/chairs/bar_chair.py index a57666d66..733cb79ac 100644 --- a/infinigen/assets/seating/chairs/bar_chair.py +++ b/infinigen/assets/seating/chairs/bar_chair.py @@ -11,6 +11,7 @@ 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 cbcc848c80b7b98d13082712ac676878e103db0a Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 693/727] Add 39 lines to infinigen/assets/seating/chairs/seats/round_seats.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../seating/chairs/seats/round_seats.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 infinigen/assets/seating/chairs/seats/round_seats.py 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..f5e309f26 --- /dev/null +++ b/infinigen/assets/seating/chairs/seats/round_seats.py @@ -0,0 +1,39 @@ +# Copyright (c) Princeton University. +# This 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') + # 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, + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': seat}, attrs={'is_active_output': True}) \ No newline at end of file From b1cfde3c592536b6015785ed17a0e22fcb234756 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 694/727] Add 2 lines to infinigen/assets/seating/chairs/seats/round_seats.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/chairs/seats/round_seats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/seating/chairs/seats/round_seats.py b/infinigen/assets/seating/chairs/seats/round_seats.py index f5e309f26..99959d77f 100644 --- a/infinigen/assets/seating/chairs/seats/round_seats.py +++ b/infinigen/assets/seating/chairs/seats/round_seats.py @@ -17,6 +17,7 @@ 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) @@ -35,5 +36,6 @@ 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 From 0e8097b19174e44ce0027c95ec3b0a255cb42a3e Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 695/727] Add 100 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- .../seating/chairs/seats/curvy_seats.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 infinigen/assets/seating/chairs/seats/curvy_seats.py 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..2756c5b25 --- /dev/null +++ b/infinigen/assets/seating/chairs/seats/curvy_seats.py @@ -0,0 +1,100 @@ +# 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_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), + 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}) + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': warparoundcurvealt}, attrs={'is_active_output': True}) + From 20cc133e6d81615af39ac47e81bdf42b719c3cf8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 696/727] Add 46 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- .../seating/chairs/seats/curvy_seats.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/infinigen/assets/seating/chairs/seats/curvy_seats.py b/infinigen/assets/seating/chairs/seats/curvy_seats.py index 2756c5b25..a17639b73 100644 --- a/infinigen/assets/seating/chairs/seats/curvy_seats.py +++ b/infinigen/assets/seating/chairs/seats/curvy_seats.py @@ -1,8 +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: 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 @@ -33,68 +37,110 @@ def generate_curvy_seats(nw: NodeWrangler): ('NodeSocketFloat', 'Top Relative Width', 0.5000), ('NodeSocketFloat', 'Top Bent', -0.2000), ('NodeSocketFloat', 'Seat Height', 0.6000), + 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}) + + + warparoundcurvealt = nw.new_node(Nodes.SetMaterial, group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': warparoundcurvealt}, attrs={'is_active_output': True}) From 610664dd30ee4e1472182ddf4da4f27b0b09476f Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 697/727] Add 4 lines to infinigen/assets/seating/chairs/seats/curvy_seats.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/seating/chairs/seats/curvy_seats.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/seating/chairs/seats/curvy_seats.py b/infinigen/assets/seating/chairs/seats/curvy_seats.py index a17639b73..128d5d1ff 100644 --- a/infinigen/assets/seating/chairs/seats/curvy_seats.py +++ b/infinigen/assets/seating/chairs/seats/curvy_seats.py @@ -37,6 +37,8 @@ def generate_curvy_seats(nw: NodeWrangler): ('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}) @@ -140,7 +142,9 @@ def generate_curvy_seats(nw: NodeWrangler): 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}) From 11f1417c3c9bd0a16293c2dc22ec0bd1d1dc00e6 Mon Sep 17 00:00:00 2001 From: Stamatis Alexandropoulos Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 698/727] Add 274 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. --- infinigen/assets/table_decorations/sink.py | 274 +++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 infinigen/assets/table_decorations/sink.py diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py new file mode 100644 index 000000000..1b3f3d64c --- /dev/null +++ b/infinigen/assets/table_decorations/sink.py @@ -0,0 +1,274 @@ +# Authors: +# - Hongyu Wen: sink geometry +# - Meenal Parakh: material assignment +# - Stamatis Alexandropoulos: taps +# - Alexander Raistrick: placeholder, optimize detail, redo cutter + + self.factory_seed = factory_seed + curvature = U(1.0, 1.0) + lower_height = U(0.00, 0.01) + + + @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 + + 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)]) + + + curve_to_mesh = nw.new_node(Nodes.CurveToMesh, + input_kwargs={'Curve': curve_line, 'Profile Curve': curve_circle_1.outputs["Curve"]}) + + + 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"]}) + + + + 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'}) + + 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}) + + + 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)}) + + + 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"]]}) + + 'Geometry': join_geometry_6, + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material}, attrs={'is_active_output': True}) + + 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}) + + + + + +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 From 32d8d3ad08e0aeecb32b55e428b4fe2e47be55b2 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 699/727] Add 225 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/table_decorations/sink.py | 225 +++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py index 1b3f3d64c..7ed402659 100644 --- a/infinigen/assets/table_decorations/sink.py +++ b/infinigen/assets/table_decorations/sink.py @@ -1,12 +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: # - Hongyu Wen: sink geometry # - Meenal Parakh: material assignment # - Stamatis Alexandropoulos: taps # - Alexander Raistrick: placeholder, optimize detail, redo cutter + self.factory_seed = factory_seed curvature = U(1.0, 1.0) lower_height = U(0.00, 0.01) + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) @staticmethod @@ -27,6 +36,56 @@ def tap_parameters(): } return params + return obj + + def finalize_assets(self, assets): + self.scratch.apply(assets) + self.edge_wear.apply(assets) + + 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) + }) + + + + 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_to_mesh = nw.new_node(Nodes.CurveToMesh, input_kwargs={ + 'Curve': set_curve_radius, + 'Profile Curve': curve_circle.outputs["Curve"], + 'Fill Caps': True + }) + + + + map_range = nw.new_node(Nodes.MapRange, + input_kwargs={'Value': separate_xyz.outputs["X"], 1: 0.2000, 3: 1.0000, 4: 2.5000}) + + 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}) + + + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_shade_smooth}, + attrs={'is_active_output': True}) + 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)), @@ -41,6 +100,17 @@ def tap_parameters(): ('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)]) + + + + quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', + input_kwargs={'Width': 0.2000, 'Height': 0.7000}) + + input_kwargs={'Curve': quadrilateral, 'Count': 19, 'Radius': 0.1000}, + attrs={'mode': 'POLY'}) + + + curve_to_mesh = nw.new_node(Nodes.CurveToMesh, @@ -82,6 +152,7 @@ def tap_parameters(): 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"]}) @@ -247,9 +318,163 @@ def tap_parameters(): 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, + }) + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material}, attrs={'is_active_output': True}) + 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), + + + quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', + input_kwargs={'Width': reroute_3, 'Height': reroute_2}) + + + + attrs={'mode': 'POLY'}) + + combine_xyz_1 = nw.new_node(Nodes.CombineXYZ, input_kwargs={ + 'X': group_input.outputs["Curvature"], + 'Y': group_input.outputs["Curvature"] + }) + + + + join_geometry_4 = nw.new_node(Nodes.JoinGeometry, + input_kwargs={'Geometry': [transform_1, curve_circle.outputs["Curve"]]}) + + + + + transform_2 = nw.new_node(Nodes.Transform, + + 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]}) + + + + + 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 + }) + + + + + 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, + + + + extrude_mesh_1 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={ + 'Mesh': fill_curve, + 'Offset Scale': group_input.outputs["Lower Height"] + }) + + + + less_than = nw.new_node(Nodes.Math, input_kwargs={0: separate_xyz.outputs["Z"], 1: 0.0000}, + attrs={'operation': 'LESS_THAN'}) + + + + input_kwargs={0: separate_xyz_1.outputs["X"], 1: group_input.outputs["Curvature"]}, + attrs={'operation': 'MULTIPLY'}) + + 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"]}) + + + 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'}) + + + transform_8 = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': quadrilateral_1, 'Translation': combine_xyz_7}) + + input_kwargs={'Curve': transform_8, 'Count': 10, 'Radius': multiply}, + attrs={'mode': 'POLY'}) + + + + multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Lower Height"], 1: -1.0000}, + attrs={'operation': 'MULTIPLY'}) + + + + + + transform_3 = nw.new_node(Nodes.Transform, input_kwargs={ + 'Geometry': extrude_mesh_3.outputs["Mesh"], + 'Translation': combine_xyz_3 + }) + + + + add_6 = nw.new_node(Nodes.Math, input_kwargs={ + 0: group_input.outputs["Depth"], + 1: group_input.outputs["WaterTapMargin"] + }) + + 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={ + }) + + set_material = nw.new_node(Nodes.SetMaterial, input_kwargs={ + 'Geometry': join_geometry_1, + }) + 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'}) From 4bd324731f6afb156c5745ad59067fdbdba2f10a Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 700/727] Add 117 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/table_decorations/sink.py | 117 +++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py index 7ed402659..acfaa0a27 100644 --- a/infinigen/assets/table_decorations/sink.py +++ b/infinigen/assets/table_decorations/sink.py @@ -8,14 +8,72 @@ # - Alexander Raistrick: placeholder, optimize detail, redo cutter +import bpy + +import numpy as np + +from infinigen.assets.utils import bbox_from_mesh +from infinigen.assets.utils.extract_nodegroup_parts import extract_nodegroup_geo + +from infinigen.core import tagging, tags as t self.factory_seed = factory_seed + self.tap_factory = TapFactory(factory_seed) + curvature = U(1.0, 1.0) + upper_height = U(0.2, 0.4) lower_height = U(0.00, 0.01) + 'ProtrudeAboveCounter': U(0.01, 0.025), + 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): self.scratch.apply(assets) self.edge_wear.apply(assets) + +class TapFactory(AssetFactory): + + def __init__(self, factory_seed): + super().__init__(factory_seed) @staticmethod @@ -36,6 +94,12 @@ def tap_parameters(): } return params + + def create_asset(self, **_): + obj = butil.spawn_cube() + obj.scale = (0.4,)*3 + obj.rotation_euler.z += np.pi + butil.apply_transform(obj) return obj def finalize_assets(self, assets): @@ -269,6 +333,7 @@ def finalize_assets(self, assets): 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}) @@ -324,6 +389,7 @@ def finalize_assets(self, assets): 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') 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), @@ -335,6 +401,9 @@ def finalize_assets(self, assets): + # 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={ @@ -342,15 +411,20 @@ def finalize_assets(self, assets): 'Y': group_input.outputs["Curvature"] }) + transform_1 = nw.new_node(Nodes.Transform, input_kwargs={'Geometry': sink_interior_border, 'Scale': combine_xyz_1}) 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) 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, @@ -394,7 +468,9 @@ def finalize_assets(self, assets): }) 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]}) extrude_mesh_1 = nw.new_node(Nodes.ExtrudeMesh, input_kwargs={ @@ -444,6 +520,7 @@ def finalize_assets(self, assets): 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]}) multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Lower Height"], 1: -1.0000}, @@ -459,6 +536,7 @@ def finalize_assets(self, assets): }) + #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"], @@ -469,6 +547,7 @@ def finalize_assets(self, assets): 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={ @@ -483,7 +562,45 @@ def finalize_assets(self, assets): 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 + }) From b25fed2b9fd327f9c9978de5fc8c1a2fab2fe9d9 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 701/727] Add 102 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/table_decorations/sink.py | 102 +++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py index acfaa0a27..6c7914a84 100644 --- a/infinigen/assets/table_decorations/sink.py +++ b/infinigen/assets/table_decorations/sink.py @@ -7,22 +7,59 @@ # - 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 + + +class SinkFactory(AssetFactory): + super(SinkFactory, self).__init__(factory_seed, coarse=coarse) + + self.dimensions = dimensions self.factory_seed = factory_seed + with FixedSeed(factory_seed): self.tap_factory = TapFactory(factory_seed) + + @staticmethod + depth = U(0.4, 0.5) curvature = U(1.0, 1.0) 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() @@ -106,6 +143,11 @@ def finalize_assets(self, assets): self.scratch.apply(assets) 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), @@ -113,7 +155,10 @@ def finalize_assets(self, assets): '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'}) @@ -121,6 +166,7 @@ def finalize_assets(self, assets): 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, @@ -128,11 +174,14 @@ def finalize_assets(self, assets): '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'}) @@ -145,11 +194,18 @@ def finalize_assets(self, assets): 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)), @@ -164,22 +220,31 @@ def finalize_assets(self, assets): ('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)]) + 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)}) @@ -208,7 +273,9 @@ def finalize_assets(self, assets): 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'}) @@ -237,6 +304,7 @@ def finalize_assets(self, assets): 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)}) @@ -390,16 +458,23 @@ def finalize_assets(self, assets): 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), + 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', @@ -413,6 +488,7 @@ def finalize_assets(self, assets): 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"]]}) @@ -421,7 +497,9 @@ def finalize_assets(self, assets): #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}) @@ -440,8 +518,11 @@ def finalize_assets(self, assets): 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}) @@ -452,8 +533,11 @@ def finalize_assets(self, assets): '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, @@ -472,22 +556,29 @@ def finalize_assets(self, assets): 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'}) @@ -506,6 +597,7 @@ def finalize_assets(self, assets): 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}) @@ -513,28 +605,36 @@ def finalize_assets(self, assets): 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']}) @@ -543,6 +643,8 @@ def finalize_assets(self, assets): 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"]}) From 523405bb8b87ca1c19ceab28908c41db5ac192af Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 702/727] Add 62 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/table_decorations/sink.py | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py index 6c7914a84..aa7ce27b7 100644 --- a/infinigen/assets/table_decorations/sink.py +++ b/infinigen/assets/table_decorations/sink.py @@ -27,6 +27,7 @@ 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): @@ -35,8 +36,33 @@ class SinkFactory(AssetFactory): self.dimensions = dimensions self.factory_seed = factory_seed with FixedSeed(factory_seed): + 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 depth = U(0.4, 0.5) @@ -104,13 +130,17 @@ def create_asset(self,i, placeholder, state=None, **params): 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 @@ -132,15 +162,39 @@ def tap_parameters(): 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) @@ -220,6 +274,8 @@ def nodegroup_water_tap(nw: NodeWrangler): ('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"]}) @@ -453,6 +509,7 @@ def nodegroup_water_tap(nw: NodeWrangler): 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}) @@ -465,6 +522,10 @@ def nodegroup_sink_geometry(nw: NodeWrangler): ('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"]}) @@ -654,6 +715,7 @@ def nodegroup_sink_geometry(nw: NodeWrangler): 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"]}) From 5bf3042807ed0e1ea011ab7143daf01e2c5332e9 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 703/727] Add 5 lines to infinigen/assets/table_decorations/sink.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/table_decorations/sink.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infinigen/assets/table_decorations/sink.py b/infinigen/assets/table_decorations/sink.py index aa7ce27b7..343554bb2 100644 --- a/infinigen/assets/table_decorations/sink.py +++ b/infinigen/assets/table_decorations/sink.py @@ -31,11 +31,13 @@ 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) @@ -65,8 +67,11 @@ def get_material_params(self): 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) From 986e65df03a9ebc238da4784441836d6a792f0bf Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 704/727] Add 198 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/table_decorations/vase.py | 198 +++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 infinigen/assets/table_decorations/vase.py diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py new file mode 100644 index 000000000..2817cf8a8 --- /dev/null +++ b/infinigen/assets/table_decorations/vase.py @@ -0,0 +1,198 @@ +# Copyright (c) Princeton University. +# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. + + + def __init__(self, factory_seed, coarse=False, dimensions=None): + if dimensions is None: + z = uniform(0.25, 0.40) + x = uniform(0.2, 0.4) * z + dimensions = (x, x, z) + + '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']) + } + bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), + scale=(1, 1, 1)) + 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): + self.scratch.apply(assets) + self.edge_wear.apply(assets) + + 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)]) + + + 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'}) + + + 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'}) + + + add = nw.new_node(Nodes.Math, + input_kwargs={0: group_input.outputs["Neck Scale"], 1: group_input.outputs["Top Scale"]}) + + + 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'}) + + input_kwargs={0: subtract_1, 1: group_input.outputs["Shoulder Thickness"]}, + attrs={'operation': 'MULTIPLY'}) + + + 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'}) + + + 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'}) + + + body_bottom = nw.new_node(Nodes.Transform, input_kwargs={ + 'Geometry': group_input.outputs["Profile Curve"], + 'Translation': combine_xyz_5, + 'Scale': group_input.outputs["Diameter"] + }) + + + multiply_8 = nw.new_node(Nodes.Math, input_kwargs={ + 0: group_input.outputs["Foot Height"], + 1: group_input.outputs["Height"] + }, attrs={'operation': 'MULTIPLY'}) + + + 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 + }) + + + + 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}) + 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'] + }) + + 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"] + }) + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material}, + attrs={'is_active_output': True}) From c7d24330a29aae0367e01377e8ae5dd56eab5a6a Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 705/727] Add 67 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/table_decorations/vase.py | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py index 2817cf8a8..e54451055 100644 --- a/infinigen/assets/table_decorations/vase.py +++ b/infinigen/assets/table_decorations/vase.py @@ -1,13 +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: 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 + +class VaseFactory(AssetFactory): def __init__(self, factory_seed, coarse=False, dimensions=None): + super(VaseFactory, self).__init__(factory_seed, coarse=coarse) + + self.dimensions = dimensions + + with FixedSeed(factory_seed): + self.params = self.sample_parameters(dimensions) + @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, @@ -24,16 +59,28 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): '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 + 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): self.scratch.apply(assets) 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), @@ -41,6 +88,7 @@ def finalize_assets(self, assets): ('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"], @@ -58,6 +106,7 @@ def finalize_assets(self, assets): 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"], @@ -82,10 +131,12 @@ def finalize_assets(self, assets): 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'}) @@ -109,9 +160,11 @@ def finalize_assets(self, assets): 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'}) @@ -119,6 +172,7 @@ def finalize_assets(self, assets): 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"], @@ -135,6 +189,7 @@ def finalize_assets(self, assets): 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"], @@ -142,12 +197,14 @@ def finalize_assets(self, assets): '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"], @@ -160,13 +217,19 @@ def finalize_assets(self, assets): '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'], @@ -187,6 +250,7 @@ def finalize_assets(self, assets): '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={ @@ -194,5 +258,8 @@ def finalize_assets(self, assets): 'Selection': lofting.outputs["Top"] }) + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': set_material}, attrs={'is_active_output': True}) + + From c578a3de111791240c65374c9b366110c8d6f46a Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 706/727] Add 31 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/table_decorations/vase.py | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py index e54451055..85f19a230 100644 --- a/infinigen/assets/table_decorations/vase.py +++ b/infinigen/assets/table_decorations/vase.py @@ -18,6 +18,7 @@ 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): @@ -27,6 +28,32 @@ def __init__(self, factory_seed, coarse=False, dimensions=None): 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 @@ -73,7 +100,9 @@ def create_asset(self, **params): return obj def finalize_assets(self, assets): + if self.scratch: self.scratch.apply(assets) + if self.edge_wear: self.edge_wear.apply(assets) @@ -258,6 +287,8 @@ def geometry_vases(nw: NodeWrangler, **kwargs): '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}) From 9d33f6b8880cb85fc5f281841b31fc1b5a1e751c Mon Sep 17 00:00:00 2001 From: Pvl Bot Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 707/727] Add 4 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Pvl Bot. --- infinigen/assets/table_decorations/vase.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py index 85f19a230..12c59ab09 100644 --- a/infinigen/assets/table_decorations/vase.py +++ b/infinigen/assets/table_decorations/vase.py @@ -24,6 +24,10 @@ 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): From c235f8d7dff7476e22c7800786d881cb25bdaa55 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 708/727] Add 1 lines to infinigen/assets/table_decorations/vase.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/table_decorations/vase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/table_decorations/vase.py b/infinigen/assets/table_decorations/vase.py index 12c59ab09..437153ed5 100644 --- a/infinigen/assets/table_decorations/vase.py +++ b/infinigen/assets/table_decorations/vase.py @@ -98,6 +98,7 @@ def create_asset(self, **params): 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) From efeec1b63fe642aa80f39d15b2c77191d1908a77 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 709/727] Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/table_decorations/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 infinigen/assets/table_decorations/__init__.py diff --git a/infinigen/assets/table_decorations/__init__.py b/infinigen/assets/table_decorations/__init__.py new file mode 100644 index 000000000..7fa6b2365 --- /dev/null +++ b/infinigen/assets/table_decorations/__init__.py @@ -0,0 +1 @@ +from .sink import SinkFactory, TapFactory From d6e7619042b5fec23f4160c8cb4b90aa99e3ba31 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 710/727] Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/table_decorations/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/table_decorations/__init__.py b/infinigen/assets/table_decorations/__init__.py index 7fa6b2365..fb5b339fa 100644 --- a/infinigen/assets/table_decorations/__init__.py +++ b/infinigen/assets/table_decorations/__init__.py @@ -1 +1,2 @@ +from .vase import VaseFactory from .sink import SinkFactory, TapFactory From e4321396f3f07d7e8931ae39e73782c469404b37 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 711/727] Add 1 lines to infinigen/assets/table_decorations/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/table_decorations/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infinigen/assets/table_decorations/__init__.py b/infinigen/assets/table_decorations/__init__.py index fb5b339fa..a58696a3a 100644 --- a/infinigen/assets/table_decorations/__init__.py +++ b/infinigen/assets/table_decorations/__init__.py @@ -1,2 +1,3 @@ from .vase import VaseFactory from .sink import SinkFactory, TapFactory +from .book import BookFactory, BookColumnFactory, BookStackFactory From 1e89e80f1a78dcd19aec0b7cdcd8e328046490c5 Mon Sep 17 00:00:00 2001 From: Yiming Zuo Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 712/727] Add 324 lines to infinigen/assets/table_decorations/utils.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. --- infinigen/assets/table_decorations/utils.py | 324 ++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 infinigen/assets/table_decorations/utils.py 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}) From 0b0a88a8221e2c3b7e4ff3a6524289b52e9e24d9 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 713/727] Add 181 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/table_decorations/book.py | 181 +++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 infinigen/assets/table_decorations/book.py diff --git a/infinigen/assets/table_decorations/book.py b/infinigen/assets/table_decorations/book.py new file mode 100644 index 000000000..c1f44dc89 --- /dev/null +++ b/infinigen/assets/table_decorations/book.py @@ -0,0 +1,181 @@ +# Copyright (c) Princeton University. +# This 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 trimesh +from numpy.random import uniform +from trimesh import proximity + +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 + + +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) + 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): + self.scratch.apply(assets) + 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 + + +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: + + 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) From 10a59e4bc937db9e386f7180ccaafe2a9a1163e0 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 714/727] Add 17 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/table_decorations/book.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/infinigen/assets/table_decorations/book.py b/infinigen/assets/table_decorations/book.py index c1f44dc89..7e906fe0b 100644 --- a/infinigen/assets/table_decorations/book.py +++ b/infinigen/assets/table_decorations/book.py @@ -5,6 +5,7 @@ import bpy import bmesh import numpy as np +import math import trimesh from numpy.random import uniform from trimesh import proximity @@ -153,6 +154,8 @@ def create_asset(self, **params) -> bpy.types.Object: 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): @@ -165,6 +168,20 @@ def __init__(self, factory_seed, coarse=False): 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 = [] From f6fb25e41204c6083f8a82a0259c197a58d274c3 Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 715/727] Add 17 lines to infinigen/assets/table_decorations/book.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/table_decorations/book.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/infinigen/assets/table_decorations/book.py b/infinigen/assets/table_decorations/book.py index 7e906fe0b..785abedce 100644 --- a/infinigen/assets/table_decorations/book.py +++ b/infinigen/assets/table_decorations/book.py @@ -10,6 +10,7 @@ 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 @@ -19,6 +20,7 @@ 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): @@ -30,6 +32,18 @@ def __init__(self, factory_seed, coarse=False): 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: @@ -39,10 +53,13 @@ def create_asset(self, **params) -> bpy.types.Object: 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): From 67d02aadc5aaca079372a92be66e5ba8a3f27495 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 716/727] Add 779 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/windows/window.py | 779 +++++++++++++++++++++++++++++ 1 file changed, 779 insertions(+) create mode 100644 infinigen/assets/windows/window.py diff --git a/infinigen/assets/windows/window.py b/infinigen/assets/windows/window.py new file mode 100644 index 000000000..dd0e461d2 --- /dev/null +++ b/infinigen/assets/windows/window.py @@ -0,0 +1,779 @@ +from numpy.random import uniform as U, normal as N, randint as RI, uniform + +from infinigen.core.util.blender import deep_clone_obj + + def __init__(self, factory_seed, coarse=False, curtain=None, shutter=None): + self.params = self.sample_parameters() + self.curtain = curtain + self.shutter = shutter + @staticmethod + def sample_parameters(): + "FrameMaterial": surface.shaderfunc_to_material(shader_frame_material_choice, vertical=True), + 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_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 + + curtain_interval_number = int(width / U(0.08, 0.2)) + curtain_mid_l = -U(0, width / 2) + curtain_mid_r = U(0, width / 2) + "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): + 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) + if max(obj.dimensions) > 8: + butil.delete(obj) + obj = obj_ + else: + butil.delete(obj_) + # 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_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'}) + + + + + transform = nw.new_node(Nodes.Transform, + input_kwargs={'Geometry': switch.outputs[6], 'Translation': combine_xyz}) + + + multiply_6 = nw.new_node(Nodes.Math, input_kwargs={ + 0: group_input_1.outputs["PanelHAmount"], + 1: group_input_1.outputs["PanelVAmount"] + }, attrs={'operation': 'MULTIPLY'}) + + input_kwargs={'Geometry': geometry_to_instance, 'Amount': multiply_6}, + attrs={'domain': 'INSTANCE'}) + + + input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: reroute}, + attrs={'operation': 'DIVIDE'}) + + + + + input_kwargs={0: duplicate_elements.outputs["Duplicate Index"], 1: reroute}, + attrs={'operation': 'MODULO'}) + + + + + 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 + }) + + + multiply_10 = nw.new_node(Nodes.Math, input_kwargs={0: group_input_1.outputs["OpenVAngle"], 1: power_1}, + attrs={'operation': 'MULTIPLY'}) + + + + multiply_11 = nw.new_node(Nodes.Math, input_kwargs={0: divide, 1: modulo_1}, + attrs={'operation': 'MULTIPLY'}) + + + + multiply_12 = nw.new_node(Nodes.Math, input_kwargs={0: divide_1, 1: modulo_2}, + attrs={'operation': 'MULTIPLY'}) + + + + 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'}) + + + + + rotate_instances_1 = nw.new_node(Nodes.RotateInstances, input_kwargs={ + 'Instances': rotate_instances, + 'Rotation': combine_xyz_5, + 'Pivot Point': combine_xyz_6 + }) + + + multiply_15 = nw.new_node(Nodes.Math, input_kwargs={0: power_2, 1: group_input_1.outputs["OpenOffset"]}, + attrs={'operation': 'MULTIPLY'}) + + + set_position_1 = nw.new_node(Nodes.SetPosition, + input_kwargs={'Geometry': rotate_instances_1, 'Offset': combine_xyz_4}) + + + 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'}) + + 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}) + + + 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 + }) + + + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={ + 'Geometry': realize_instances, + 'Bounding Box': bounding_box.outputs["Bounding Box"] + }, attrs={'is_active_output': True}) + 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'}) + + + multiply_2 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Width"], 1: -0.5000}, + attrs={'operation': 'MULTIPLY'}) + + + + + duplicate_elements = nw.new_node(Nodes.DuplicateElements, input_kwargs={ + 'Geometry': geometry_to_instance, + 'Amount': group_input.outputs["Amount"] + }, attrs={'domain': 'INSTANCE'}) + + + + divide = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["Height"], 1: add_1}, + attrs={'operation': 'DIVIDE'}) + + + + 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}) + + 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)]) + + + + + 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'}) + + + + + + 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"]}) + + + combine_xyz_8 = nw.new_node(Nodes.CombineXYZ, + input_kwargs={'X': multiply_2, 'Z': group_input.outputs["FrameDepth"]}) + + + join_geometry_3 = nw.new_node(Nodes.JoinGeometry, + input_kwargs={'Geometry': [curve_line, curve_line_4, curve_line_3]}) + + + curve_to_mesh_1 = nw.new_node(Nodes.CurveToMesh, input_kwargs={ + 'Curve': join_geometry_3, + 'Profile Curve': curve_circle.outputs["Curve"], + 'Fill Caps': True + }) + + + + 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'}) + + + 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"] + }) + + + + + + + + + + join_geometry_1 = nw.new_node(Nodes.JoinGeometry, + input_kwargs={'Geometry': [resample_curve, resample_curve_1]}) + + + capture_attribute = nw.new_node(Nodes.CaptureAttribute, input_kwargs={ + 'Geometry': join_geometry_1, + 2: spline_parameter_1.outputs["Factor"] + }) + + + 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'}) + + + + multiply_6 = nw.new_node(Nodes.Math, input_kwargs={0: sine, 1: group_input.outputs["Depth"]}, + attrs={'operation': 'MULTIPLY'}) + + + set_position = nw.new_node(Nodes.SetPosition, input_kwargs={ + 'Geometry': capture_attribute.outputs["Geometry"], + 'Offset': combine_xyz_2 + }) + + + quadrilateral = nw.new_node('GeometryNodeCurvePrimitiveQuadrilateral', + input_kwargs={'Width': reroute, 'Height': 0.0020}) + + + + 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_to_mesh_2 = nw.new_node(Nodes.CurveToMesh, input_kwargs={ + 'Curve': curve_line, + 'Profile Curve': curve_circle_1.outputs["Curve"] + }) + + + + set_position_4 = nw.new_node(Nodes.SetPosition, + input_kwargs={'Geometry': curve_to_mesh_2, 'Offset': combine_xyz_10}) + + 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}) + 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"] + }) + + + 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}) + + 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"] + }) + + + 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'}) + + + shutter_number = nw.new_node(Nodes.Math, input_kwargs={0: floor, 1: 1.0000}, label='ShutterNumber', + attrs={'operation': 'SUBTRACT'}) + + 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'}) + + + + + set_position = nw.new_node(Nodes.SetPosition, input_kwargs={ + 'Geometry': duplicate_elements.outputs["Geometry"], + 'Offset': combine_xyz_1 + }) + + + + 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"] + }) + + + multiply_4 = nw.new_node(Nodes.Math, input_kwargs={0: group_input.outputs["ShutterWidth"]}, + attrs={'operation': 'MULTIPLY'}) + + + + geometry_to_instance_1 = nw.new_node('GeometryNodeGeometryToInstance', + input_kwargs={'Geometry': curve_line}) + + + rotate_instances_1 = nw.new_node(Nodes.RotateInstances, input_kwargs={ + 'Instances': geometry_to_instance_1, + 'Rotation': combine_xyz_4 + }) + + + + 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}) + + + group_output = nw.new_node(Nodes.GroupOutput, input_kwargs={'Geometry': realize_instances_1}, + attrs={'is_active_output': True}) + + 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"] + }) + + + 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}) + + + lineseq = nw.new_node(nodegroup_line_seq().name, input_kwargs={ + 'Width': group_input.outputs["Width"], + 'Height': group_input.outputs["Height"], + 'Amount': add + }) + + + 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}) + + + 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)}) + + + 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"] + }) + + + 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"] + }) + + + 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}) + + 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}) + + 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}) + + 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}) + + 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}) From aa81a551d0de4089301d132adeac4b876386d839 Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:35 -0700 Subject: [PATCH 717/727] Add 203 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/windows/window.py | 203 +++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/infinigen/assets/windows/window.py b/infinigen/assets/windows/window.py index dd0e461d2..580dcb962 100644 --- a/infinigen/assets/windows/window.py +++ b/infinigen/assets/windows/window.py @@ -1,14 +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. + + +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.placement.factory import AssetFactory + +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.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), + } + return params + def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter=None): if dimensions is None: width = U(1, 4) @@ -66,6 +126,7 @@ def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter= } def create_asset(self, dimensions=None, open=None, realized=True, **params): + obj = butil.spawn_cube() butil.modify_mesh( obj, 'NODES', @@ -82,6 +143,11 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): obj = obj_ else: butil.delete(obj_) + 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), @@ -185,7 +251,9 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -195,33 +263,47 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -234,21 +316,28 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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, @@ -259,8 +348,11 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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, @@ -268,14 +360,17 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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'}) @@ -283,6 +378,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -307,6 +403,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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}) @@ -320,12 +417,20 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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)]) @@ -335,24 +440,32 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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"], @@ -362,6 +475,11 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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), @@ -369,8 +487,11 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): ('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'}) @@ -378,9 +499,13 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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"], @@ -390,14 +515,17 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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, @@ -405,7 +533,9 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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"], @@ -418,6 +548,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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}) @@ -427,23 +558,33 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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'}) @@ -454,22 +595,28 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -501,17 +648,22 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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"]]}) @@ -520,6 +672,12 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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), @@ -534,6 +692,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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'}) @@ -546,6 +705,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -555,6 +715,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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"]}) @@ -567,10 +728,12 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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'}) @@ -585,15 +748,20 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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}) @@ -610,22 +778,28 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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"], @@ -643,10 +817,16 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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), @@ -661,6 +841,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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'}) @@ -673,6 +854,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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"], @@ -680,6 +862,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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'}) @@ -690,6 +873,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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"], @@ -700,6 +884,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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}) @@ -724,6 +909,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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"], @@ -736,6 +922,7 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): '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"], @@ -749,6 +936,10 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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), @@ -758,18 +949,30 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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 From a9947f60ff4a09b09826a9df44cab38395bf8a74 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 718/727] Add 51 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/windows/window.py | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/infinigen/assets/windows/window.py b/infinigen/assets/windows/window.py index 580dcb962..bbd7af223 100644 --- a/infinigen/assets/windows/window.py +++ b/infinigen/assets/windows/window.py @@ -1,6 +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: +# - Hongyu Wen: primary author +# - Alexander Raistrick: update window glass import bpy import random @@ -15,8 +18,31 @@ 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): @@ -24,6 +50,7 @@ def __init__(self, factory_seed, coarse=False, curtain=None, shutter=None): with FixedSeed(factory_seed): self.params = self.sample_parameters() + self.beveler = BevelSharp() self.curtain = curtain self.shutter = shutter @@ -66,6 +93,7 @@ def sample_parameters(): "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 @@ -104,10 +132,14 @@ def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter= 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, @@ -127,6 +159,7 @@ def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter= def create_asset(self, dimensions=None, open=None, realized=True, **params): obj = butil.spawn_cube() + butil.modify_mesh( obj, 'NODES', @@ -138,11 +171,29 @@ def create_asset(self, dimensions=None, open=None, realized=True, **params): 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 From b4cd5349719f9362028015474d0a10843ff605d6 Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 719/727] Add 2 lines to infinigen/assets/windows/window.py. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/assets/windows/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/windows/window.py b/infinigen/assets/windows/window.py index bbd7af223..d01d38be6 100644 --- a/infinigen/assets/windows/window.py +++ b/infinigen/assets/windows/window.py @@ -120,6 +120,8 @@ def sample_asset_params(self, dimensions=None, open=None, curtain=None, shutter= 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 From fccf8ed9dd765f1dda9c42fff87b8b58f8f03b9b Mon Sep 17 00:00:00 2001 From: Hongyu Wen Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 720/727] Add 6 lines to infinigen/assets/windows/__init__.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. --- infinigen/assets/windows/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 infinigen/assets/windows/__init__.py 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 From 3d13d4acd8f930a441e41a137669d4cd52c0d45e Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 721/727] Add 1 lines to infinigen/assets/decor/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/decor/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 infinigen/assets/decor/__init__.py 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 From cf6af5bb99919796d19577e6dcda2c295dac00e8 Mon Sep 17 00:00:00 2001 From: Lingjie Mei Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 722/727] Add 97 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. --- infinigen/assets/decor/aquarium_tank.py | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 infinigen/assets/decor/aquarium_tank.py diff --git a/infinigen/assets/decor/aquarium_tank.py b/infinigen/assets/decor/aquarium_tank.py new file mode 100644 index 000000000..6fe7effe9 --- /dev/null +++ b/infinigen/assets/decor/aquarium_tank.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 numpy as np +from numpy.random import uniform + +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.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 + + +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) + + + self.scratch = None + 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') + self.edge_wear.apply(assets) From ca0b43c24ac67def9b51cb2499425f75d39358ec Mon Sep 17 00:00:00 2001 From: Meenal Parakh Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 723/727] Add 15 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. --- infinigen/assets/decor/aquarium_tank.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/infinigen/assets/decor/aquarium_tank.py b/infinigen/assets/decor/aquarium_tank.py index 6fe7effe9..14aca7f1f 100644 --- a/infinigen/assets/decor/aquarium_tank.py +++ b/infinigen/assets/decor/aquarium_tank.py @@ -19,6 +19,7 @@ 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): @@ -37,8 +38,18 @@ def __init__(self, factory_seed, coarse=False): 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: @@ -94,4 +105,8 @@ def make_belts(self): 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) From fdca5f462d54a64194a8e5f603ae16989fe44670 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 724/727] Add 2 lines to infinigen/assets/decor/aquarium_tank.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- infinigen/assets/decor/aquarium_tank.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infinigen/assets/decor/aquarium_tank.py b/infinigen/assets/decor/aquarium_tank.py index 14aca7f1f..627c5db35 100644 --- a/infinigen/assets/decor/aquarium_tank.py +++ b/infinigen/assets/decor/aquarium_tank.py @@ -6,10 +6,12 @@ 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 da31665539206f4941f8c8645fbf12a400f2b944 Mon Sep 17 00:00:00 2001 From: Alexander Raistrick Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 725/727] Add 10 lines to infinigen/datagen/configs/indoor_background_configs.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. --- .../datagen/configs/indoor_background_configs.gin | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 infinigen/datagen/configs/indoor_background_configs.gin 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 From 8b50b2042f5b0aa08a0e65fe1563c0cdc87fd2cc Mon Sep 17 00:00:00 2001 From: David Yan Date: Mon, 17 Jun 2024 17:24:36 -0700 Subject: [PATCH 726/727] Add 3 lines to infinigen/datagen/configs/export.gin. Contributed as part of Infinigen-Indoors by David Yan. --- infinigen/datagen/configs/export.gin | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 infinigen/datagen/configs/export.gin 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 From e9ce81acc6b25d7bfbb5522bc75f97e90edd3228 Mon Sep 17 00:00:00 2001 From: pvl-bot Date: Mon, 17 Jun 2024 23:51:41 -0700 Subject: [PATCH 727/727] v1.4.0 - Preliminary release of Infinigen Indoors --- README.md | 108 +- docs/HelloRoom.md | 1 - docs/Installation.md | 4 +- docs/images/hello_room/dining.png | Bin 0 -> 1378685 bytes docs/images/hello_room/dining_blender.png | Bin 0 -> 354103 bytes docs/images/hello_room/dining_depth.png | Bin 0 -> 41737 bytes docs/images/hello_room/dining_obj.png | Bin 0 -> 13342 bytes docs/images/infinigen.png | Bin 0 -> 27917 bytes log.txt | 18262 ++++++++++++++++++++ pyproject.toml | 2 +- 10 files changed, 18352 insertions(+), 25 deletions(-) create mode 100644 docs/images/hello_room/dining.png create mode 100644 docs/images/hello_room/dining_blender.png create mode 100644 docs/images/hello_room/dining_depth.png create mode 100644 docs/images/hello_room/dining_obj.png create mode 100644 docs/images/infinigen.png create mode 100644 log.txt 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) -[![Infinigen Intro Video](docs/images/video_thumbnail.png)](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:

Infinite Photorealistic Worlds using Procedural Generation

-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. - +

Infinigen Indoors: Photorealistic Indoor Scenes using Procedural Generation

- - - - +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/HelloRoom.md b/docs/HelloRoom.md index c35e23611..bbace7ba0 100644 --- a/docs/HelloRoom.md +++ b/docs/HelloRoom.md @@ -11,7 +11,6 @@ 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. diff --git a/docs/Installation.md b/docs/Installation.md index c713017cc..d5152facd 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -68,10 +68,10 @@ conda activate infinigen Then, install the infinigen package using one of the options below: ```bash -# Minimal install (No terrain or opengl GT: ok for Infinigen-Indoors or single-object generation) +# Minimal install (No terrain or opengl GT, ok for Infinigen-Indoors or single-object generation) INFINIGEN_MINIMAL_INSTALL=True pip install -e . -# Full install (Terrain & OpenGL-GT enabled; Needed for Infinigen-Nature HelloWorld) +# Full install (Terrain & OpenGL-GT enabled, needed for Infinigen-Nature HelloWorld) pip install -e . # Developer install (includes pytest, ruff, other recommended dev tools) diff --git a/docs/images/hello_room/dining.png b/docs/images/hello_room/dining.png new file mode 100644 index 0000000000000000000000000000000000000000..b0478f7cdc6b2cb90c5563e03294b7ed5eec2d73 GIT binary patch literal 1378685 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
d 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⪼Z`5b_ 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�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

#)F@Vyl5yeL!AMVg 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%0Dt{Zb)?p+VqWAAwDaAe?(7T(y6i3bC|Q_kjcsJ( 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 z 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 z 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

A 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

ycAh}|aCveJE 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

    ^nAXFaVw>$p<1urSC!lk>C^ik&7+wJKj9(LodK> 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 DO8k`d literal 0 HcmV?d00001 diff --git a/docs/images/hello_room/dining_depth.png b/docs/images/hello_room/dining_depth.png new file mode 100644 index 0000000000000000000000000000000000000000..f43784fc38cdb49725ad0351e24b89ab4e6e22a3 GIT binary patch literal 41737 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 hP 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 literal 0 HcmV?d00001 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 +Date: Sun Jun 16 23:16:26 2024 -0700 + + Add 60 lines to infinigen/assets/clothes/pants.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 04f9c87338fc11cf1e51faf34086894d3f4cf01a +Author: pvl-bot +Date: Sun Jun 16 23:16:26 2024 -0700 + + Add 2 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 29fe4d06acf7a2a939df8ee43f58613afa989a7c +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 3 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 2f2d8de76790e0cd3680d4a9f3571825586b2ed2 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 21 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Mingzhe Wang. + +commit e32838a35a025ab3cf247396ea093f04261be5d8 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 50 lines to infinigen/assets/materials/woods/wood_old.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 270216a50b244df40a91790bfd101ef705065868 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 15 lines to infinigen/assets/materials/woods/wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 69c9c5cbfb2a45e8ebc23d0a9db15c18b7b3aae7 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 12 lines to infinigen/assets/materials/woods/hexagon_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit ed6fb7f2182f299215b79cbf56fd9e9559a14cde +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 18 lines to infinigen/assets/materials/woods/composite_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 1b8d1fa021ea40e8e66e5c0ebc70f89827afc08f +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 22 lines to infinigen/assets/materials/woods/non_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 5fdad3f7345bff430745b5636addf9eb74609473 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 14 lines to infinigen/assets/materials/woods/square_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 87cb2917af93261415e1287b41f56d4114138079 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 6 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 182bc877b764dc7cf01219841e5c02a4ee1998ea +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 7 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 62d5b7c3c606c680d0354d8c6634142e9d96fc44 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 133 lines to infinigen/assets/materials/woods/wood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 8814ffb16eaa9388e2b94ba25848803e484270fa +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 12 lines to infinigen/assets/materials/woods/staggered_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 86dbcb903bcb1e89949575f36ad7d3aacae7bae8 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 14 lines to infinigen/assets/materials/woods/crossed_wood_tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 230bf146b4172180487ad2f7b9cb2e50a71c2977 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 3 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit f1ec5bf90e94173b0ff4968f6595917efeae255e +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 11 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Beining Han. + +commit dd0493e9c617a1454769d11c42e7546f1f2b9711 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 58 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 6a5e880802f69cf527aa0408b4ed7959f270fc5b +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 116 lines to infinigen/assets/materials/woods/tiled_wood.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 9109359d3becc8acaddbb635e0f5a279b0cdccf5 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 63 lines to infinigen/assets/materials/stone_and_concrete/concrete.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 25bc986e937c6dc04fe1448cd6c7bc6d4093fc2a +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 179 lines to infinigen/assets/materials/stone_and_concrete/concrete.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 6b8a4f2172bcbaeb5d86d030cadf587940996bc0 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 20 lines to infinigen/assets/materials/metal/brushed_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit e3d2916af692425917bcf36b290d1fef9ebef25c +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 67 lines to infinigen/assets/materials/metal/brushed_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 434a7339f13889e9a700164f7a469dee7b5e7313 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 20 lines to infinigen/assets/materials/metal/grained_and_polished_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 9776d5acb7109f62b18fe624f76215b305947f6c +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 59 lines to infinigen/assets/materials/metal/grained_and_polished_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 49651d254f283c736ac078d74b57004ce5374269 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 56 lines to infinigen/assets/materials/metal/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit cd97d6110b0c621d6b8654eefe3762483d2add75 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 26 lines to infinigen/assets/materials/metal/hammered_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 99d5fbf2d48830bc5f81feae3e3d86bde2a1c4c7 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 55 lines to infinigen/assets/materials/metal/hammered_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit fa2a7bd0676d76068a85a7143d16379db24cb4b7 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 33 lines to infinigen/assets/materials/metal/metal_basic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 4a0e6b1601bba78408cee8f9ed122fda0a4310ff +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 19 lines to infinigen/assets/materials/metal/galvanized_metal.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit dc1cfcdaf9cb5fba5ab15962259364cc488cd1ba +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 49 lines to infinigen/assets/materials/metal/galvanized_metal.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 35a7a0d73b252ada8051b81b273122b4ec1bfc39 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 1 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 08b4e3f3792d80185d9553b327cecb79a5c52bff +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 13 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 01212dcf30bdd702a07563056f65cfd3b5cb0f37 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 69 lines to infinigen/assets/materials/plastics/plastic_rough.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 7028289d869e51f2b5b401a9e094bbc55f550dd9 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 2 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8998f3f6ae9a438e01ef41b9815d683cf557f4a9 +Author: pvl-bot +Date: Sun Jun 16 23:16:25 2024 -0700 + + Add 7 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 422618540da609c99c90b5ec936834d812d2d07a +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 35 lines to infinigen/assets/materials/plastics/plastic_translucent.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 25f92e17c68283fa4ebd148ae969bb240e189b53 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 8d76a819c133e746c39b54cb2d37d12b9bcf86de +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 266 lines to infinigen/assets/materials/leather_and_fabrics/coarse_knit_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 6ab9c9405173b17a62ee63c7ac1b13b5ab0439bc +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 8 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 0536a0f7b08c398cffe0ecef394e39fc7f479c51 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 28 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit a72d93218ed3a1e92f48c79f370145a3e2024e6d +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 74 lines to infinigen/assets/materials/leather_and_fabrics/leather.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit de22aa971664c751581e37eaf12089bc07e1f638 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ecf69bb1f2934d38c8dfa8bb66c6c17d5b540b11 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 41 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 044649819bcbde3c61687c1106cdcbd841388bc8 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 125 lines to infinigen/assets/materials/leather_and_fabrics/general_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 94bd36e7ad9555aa74e6e48bad8bd6e80b97b623 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/lined_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit b6ea7077be962e2677df5ff100dadbbc1e5d62b9 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 163 lines to infinigen/assets/materials/leather_and_fabrics/lined_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 1f17a053a81109337d59962a958c9cc39c934d62 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 2 lines to infinigen/assets/materials/leather_and_fabrics/__init__.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 718e4825af397b0f20ecc5363fadad9b760fdbdc +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 20 lines to infinigen/assets/materials/leather_and_fabrics/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 9f636a2bc1a163b5a92f74673d8efa1e108fb7e1 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/velvet.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 7e447c1d87fff59bacb4faecd168acc55521f409 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 82 lines to infinigen/assets/materials/leather_and_fabrics/velvet.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit c95602c75ec9045e331ee2b3a4bd959c63f91969 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 9cfad3da986577c10f7e2ee06bfec6bb669a387f +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 36 lines to infinigen/assets/materials/leather_and_fabrics/sofa_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 66c7d007676ce0b50e9060613de3121662d89c17 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 4 lines to infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 9bfe404beab82aab070178a3f79d057e9aa87062 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 150 lines to infinigen/assets/materials/leather_and_fabrics/fine_knit_fabric.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 554fc1ce2d8956f305df18d8bd27a9a0cae43c6d +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 11 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit c905341ad3aca7b66ab817256775fdb2cb38dc67 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 14 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 1e3f0d9b2d3f057559bedfb7e3077b9f7453ff5f +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 145 lines to infinigen/assets/materials/wear_tear/procedural_scratch.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 728397b670eb020cd3322beb7ed13525af8b2d55 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 11 lines to infinigen/assets/materials/wear_tear/procedural_edge_wear.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d6b4188a0558f548fc68e1f15f5660f08b3ddbae +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 265 lines to infinigen/assets/materials/wear_tear/procedural_edge_wear.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 44c1d4afbf120f2882ee8c9ece9cddecf849298b +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 53 lines to infinigen/assets/materials/marble_voronoi.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 7e4792a75401cefa966046d139592178c11fec35 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 1 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit b619525c8e481ec3034ef8265c65eb927680fb43 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 7 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 4a0ff94693627c26f9eeecd73c7090ede6e458a9 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 150 lines to infinigen/assets/materials/table_marble.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 785feb8460bdb80476de0eccaebcd2970026cb02 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 64 lines to infinigen/assets/materials/brick.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 669baf98f5942c0f9b6a8f754ef7d55e111cda99 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 10 lines to infinigen/assets/materials/fabrics.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 4d1e999a114e3b51fec2dfd7fa1b20f045426e90 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 37 lines to infinigen/assets/materials/invisible_to_camera.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit aa98111c4223a788a8bb0450aefe483a1dc43ba9 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 5 lines to infinigen/assets/materials/black_plastic.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit 9b07936f45b5653eafb07442a1bbafedc2f6e9f8 +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 17 lines to infinigen/assets/materials/black_plastic.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 7b9da237a6ec2a27b05268a313ea8e0c2ffcec3d +Author: pvl-bot +Date: Sun Jun 16 23:16:24 2024 -0700 + + Add 5 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 368c5ea783509e9fa789654abe75627a4aba3524 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 53 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit bc9fbbb58d9bd1e8504daa21c34f65a12efcfaba +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 105 lines to infinigen/assets/materials/table_materials.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 45503a50ced952138b604ef3e52dd245343b1702 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 3 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 02bee5beb4c026b817ec6a1ba66b08fd2ba5db10 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 6 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 9948be401b60810749aa7720186a9552f368cb70 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 50 lines to infinigen/assets/materials/plaster.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit d73a7f4143e59190561dc75aaacd076f38344dbd +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 5 lines to infinigen/assets/materials/microwave_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit f2f91c74084a7a626d79da55de67279637cafedf +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 22 lines to infinigen/assets/materials/microwave_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit ca81a3410a4ea78a4e7f0f70fd3e21febece52f4 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 21 lines to infinigen/assets/materials/glass.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit fe7ea88ee5d89542ed68203818f8ce0ac29008ed +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 27 lines to infinigen/assets/materials/glass.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 25de7cf093bcefa6e47b001a1d95bb0c564214ba +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 12 lines to infinigen/assets/materials/text_no_barcode.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 2c448bd7600ee5f9325237e0105890cde4f15f0a +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 1 lines to infinigen/assets/materials/art.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 161ad8dbf1fac7abe2fbd7cc9af8dea752fed9ca +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 99 lines to infinigen/assets/materials/art.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit d05b25800d93ce04ac9b6f8a72b4fc7c778ebf2d +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 1 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 34ea11ac19760c874bb3c0dad730e28f5bdc60e4 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 5 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit 29adf5c572746c0f17c270c5964025386675c08d +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 39 lines to infinigen/assets/materials/ceiling_light_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 2bf4f659948e09831b3625229f592a02ccce93fe +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 8 lines to infinigen/assets/materials/rug.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit ffbc4298fec243eb63aec2779075f0ba193904fd +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 42 lines to infinigen/assets/materials/rug.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 36b4f6a2a84e5fe60ad21cb808fa50908a3c6116 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 27 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit daf6aa10469f3010be72e2f52051b352e4ab8133 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 35 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 79fa245b06c84f260a3fd8e427a38414a2b09670 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 247 lines to infinigen/assets/materials/text.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit abea932da4a82b93122c454301986119a5acff1d +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 1 lines to infinigen/assets/materials/marble.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 4ea0527e45aa8ce28b1643776dd7f4a7cdde13ef +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 9 lines to infinigen/assets/materials/marble.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 3cac6eaf59ecfda77ffdc0bcb2a72e950db8e586 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 1 lines to infinigen/assets/materials/ceramic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 88f1565d033d51c821eb14d7deb44013d3ec4757 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 43 lines to infinigen/assets/materials/ceramic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 626505ff8fdadd7f0f392f2d11c4a106c8e1e85c +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 3 lines to infinigen/assets/materials/common.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit baa9da34a355c6256aa18c8fd682a047304d4e52 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 65 lines to infinigen/assets/materials/common.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 623a2e8a3c7d74ef8b96c78fb1075a3565ec5fef +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 1 lines to infinigen/assets/materials/vase_shaders.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 90571bcc82435659db0261f1213e1fae0edb0370 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 36 lines to infinigen/assets/materials/vase_shaders.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit e77d9aeeb5338974bf10fac68ef51154f62fc543 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 5 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit a683eeb235412d161222ace548610522eccddf31 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 7 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit f09f240257e0806006129709a80a0fc5df1546b7 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 16 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 163fa12dd8df4f253a68657c333146618302d92d +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 275 lines to infinigen/assets/materials/shelf_shaders.py. Contributed as part of Infinigen-Indoors by Beining Han. + +commit f0ac5fe836f35d8f197c02eb8f6cbfe43f3354d6 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 2 lines to infinigen/assets/materials/plastic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit cb5f1e2485ab82f1dbead62c4e83ca95b60c6491 +Author: pvl-bot +Date: Sun Jun 16 23:16:23 2024 -0700 + + Add 22 lines to infinigen/assets/materials/plastic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 662ed46607923119413dfff18928792e5fad305c +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 5 lines to infinigen/assets/materials/oven_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit 155c1e63c33675d672ff29e77f9ec01983c1a13f +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 23 lines to infinigen/assets/materials/oven_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit e1c0fbaaa06ef8207a02bf7e5007f562ca5a1ed5 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 5 lines to infinigen/assets/materials/lamp_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit 157452f95a2e2bbb08e99500e3e065b895dafa02 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 54 lines to infinigen/assets/materials/lamp_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 7ea3d7ba135246749cd4c95f603be04b3fcd68fb +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 58 lines to infinigen/assets/materials/marble_regular.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit b66bab6b1aaa7f6be8eab42b73da79bf612f5d63 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 5 lines to infinigen/assets/materials/beverage_fridge_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit 06c60da8843900b93dbd23e7e2e662b9b3a42a19 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 41 lines to infinigen/assets/materials/beverage_fridge_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 01c4f5a3f8046326141fcc1d3836ed93cea3ea04 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 4 lines to infinigen/assets/materials/glass_volume.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit acb9c4694f7d8c11a584cdc08266373822021003 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 27 lines to infinigen/assets/materials/glass_volume.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit f08a1c209574bc569ef841d235169fb436057f4c +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 5 lines to infinigen/assets/materials/dishwasher_shaders.py. Contributed as part of Infinigen-Indoors by Hongyu Wen. + +commit bd90bc19f48df70535c4196aed26cb38c15b6f47 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 80 lines to infinigen/assets/materials/dishwasher_shaders.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit dcfa1c29bb05278f3ab7d86a501c5cce07e3c8f2 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 176 lines to infinigen/assets/materials/bumpy_rubber_floor.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit c93dbdab8efb5b6138c917f7b4e532e55bd91913 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 1 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit e45420e702f79fdda176e932f569fb150ee4d093 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 1 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 7b2acadf7bb95301811034ad68684ac23bff1ef8 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 44 lines to infinigen/assets/materials/hardwood_floor.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 23ef5ddc787e100803fe4a52269d9e32fd74802d +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 18 lines to infinigen/assets/materials/mirror.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 74ea6a9f3f62f438ec9f4745d08a2124ed797126 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 5 lines to infinigen/assets/materials/tile.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 3dc64c65e87964af1abce0b32cfdb3d15fe9eeeb +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 341 lines to infinigen/assets/materials/tile.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 6334b54b59d4f17a5a14b5c6648e53fb3f65f52f +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 36 lines to infinigen/assets/tables/legs/straight.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 34eea1a7bf88503c3784bd3f35baa5264bf916c6 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 74 lines to infinigen/assets/tables/legs/square.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 07696a08c31117c138d90da8e2e2a28df77ed92e +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 217 lines to infinigen/assets/tables/legs/wheeled.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 7bd75d733ebd353b5f14b0bf7768a31172b5e9e0 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 34 lines to infinigen/assets/tables/legs/single_stand.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit b79a3df48e5787298cf047280195fab9b2308a10 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 33 lines to infinigen/assets/tables/strechers.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 6bedc524d60a0939de4fabcdcd7f5b95a4529cbd +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 1 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit e94b3c4fdc0f06ef65acef1072d58b0f8e97af0f +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 30 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 1de873bebd4176144d09d8f49cbc665000fb0432 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 35 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 0f331061e48d1f7d39e8d38e71a63c472ccc80eb +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 37 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 6e23bae05216fb4cad0db15070a32afb3aea5439 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 166 lines to infinigen/assets/tables/cocktail_table.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit dbc798cbca7c2f15248c2b1ae44d1c3c435461a0 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 1 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit da3d71a574794dc9d2dddd46d2cd3fa20b39dc85 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 41 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 0859b8e211653dbb3902922aa300a3d2fbd4eefa +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 44 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d7ce755847f31a2b105991f833cca2402131c05e +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 88 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit df7ec4d336dc36c67b005f640a1471420dea82ff +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 138 lines to infinigen/assets/tables/dining_table.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 456ec73ebc83cceab93e050bdb668d3428167148 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 1 lines to infinigen/assets/tables/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 6ff2e6edc6a702c37adfbb679e197b1173190b52 +Author: pvl-bot +Date: Sun Jun 16 23:16:22 2024 -0700 + + Add 6 lines to infinigen/assets/tables/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 39e2d65e49c7cbdd8ed87781e69ba9c30e6b43e1 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 298 lines to infinigen/assets/tables/lofting.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 30056c11bb0a78d907c8c81d9b5815c6c5cfd866 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 3 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 00ae17f4b184adba788947ef34f5c8a81300d10f +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 4 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit fedf45dc993f1847ecca50a8be5c24700c162500 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 167 lines to infinigen/assets/tables/table_top.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 31eae18d55fb37104355b808019c00352f143c77 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 514 lines to infinigen/assets/tables/table_utils.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 41ba5074f3d76dcd64b8672273a483eee3696e63 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 12 lines to infinigen/assets/tableware/lid.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 8f3eccf631ae70d9d4735e562d63c19a584c8622 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 111 lines to infinigen/assets/tableware/lid.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 58eae8af3a1db681e4e1bf8e6270ab8f2c7888e3 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 100 lines to infinigen/assets/tableware/pot.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit bed052a83d7bd49dc60bc3f7a98c3dbe2c63ed7c +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 2 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit e00ef1a5a34b07fa0348b15ac0c4af0c9585d63c +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 13 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 3d510db7b67a9f12aa1d5a9e19522d2879d01037 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 101 lines to infinigen/assets/tableware/base.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 28756d3082c93daf6c5a0fbf7b017a68cfa451e2 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 4 lines to infinigen/assets/tableware/wineglass.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit dfa8aba194405348662dfc8394c758883815fe37 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 46 lines to infinigen/assets/tableware/wineglass.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 5fc7345dbba81c554b2264734d23931ea99cf76f +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 6 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 81213a80d9a8837109a1dc3720addefeceb9cc7a +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 7 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 3e1e76a974e3ef26c4594ae069f90985dadb5403 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 18 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 232931b04c7726d350687d1f76a69fb1dbc1b742 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 98 lines to infinigen/assets/tableware/plant_container.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit a2449e6096bd0c972a37fc03d4e821747889c223 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 15 lines to infinigen/assets/tableware/bottle.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 568fa794ed7e643471cca4438bea4e87d0f863f5 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 131 lines to infinigen/assets/tableware/bottle.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit c3043e33a28c198f876ecb3ee14eb12200c1347b +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 3 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 28b0e537b98740d1e3878b67df2a61bb4b30eaae +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 3 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 5d531b840e1334d281f9df20241f0a3e2cf803bb +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 4 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 758022df9c287a500eaa44920b9f2072ebdc6db5 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 119 lines to infinigen/assets/tableware/pan.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 33d22a0d831ff9ec9df361c48a868db72832280c +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 1c39257116f03a9a56fb858f3dfd9cc94b486089 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 931d458ceec9a61392e04facd4ed9cf233c49001 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 49 lines to infinigen/assets/tableware/bowl.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 6e775b4b281d33ecbc964ce76a0225dad8ab1ad9 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 14 lines to infinigen/assets/tableware/can.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 25af5f99bedbe39c02b648062c3355cea388f307 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 83 lines to infinigen/assets/tableware/can.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 25d2c99251caa9ee977030ddeecbb6eba43db6e9 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/spoon.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit c344270973d7f372206e396cca6f6aa45a3085ee +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 57 lines to infinigen/assets/tableware/spoon.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 1f218a8f0bd3dd3d0d0273c5a43ed93f9a970b70 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 22 lines to infinigen/assets/tableware/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 5e633172711e09a604f965b8bea807dff992b37a +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 8 lines to infinigen/assets/tableware/food_box.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8653d8223c18cf82bb157188f2e747fdbe97a889 +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 29 lines to infinigen/assets/tableware/food_box.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 617715fd88dc95d612af8e10219094f2273a5a9b +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/fork.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit c8ac06ba5530bf23319d807fc7b133f7f77772ed +Author: pvl-bot +Date: Sun Jun 16 23:16:21 2024 -0700 + + Add 88 lines to infinigen/assets/tableware/fork.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 2cb27e1efe6753bf560dddf9ce36812bb6f7bfd3 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit fa14d39a4c2f9f1a59eb11ae3948356b7e5ca868 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 5 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 7407655e2b3695e7baba18c345a50e6230236f39 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 77 lines to infinigen/assets/tableware/food_bag.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 46099b6335b9b81c890c82ae5d5b7d3dc7f15605 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 13 lines to infinigen/assets/tableware/cup.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit c541075ff1ad6c2e1c6579611491edf008f7f25d +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 123 lines to infinigen/assets/tableware/cup.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 2e2cc4c568700a98ccd69ead8abb0d2be3146fa3 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 13 lines to infinigen/assets/tableware/jar.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 6a04e800b79aa022968b5e3186f3ed51fbc00117 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 70 lines to infinigen/assets/tableware/jar.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 493f5e2fc34c7b8674c7305e71e584cf43a6709a +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 44 lines to infinigen/assets/tableware/plate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit c25d30e9ae2a02e73a6cfa1adbddae95124bcd4e +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 1 lines to infinigen/assets/tableware/knife.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit c52c2fcb150e4f7d61b8ec577984417e4cfb52ef +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 94 lines to infinigen/assets/tableware/knife.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit cf2c2e7a3f869328193a5029089792e2d1ea3705 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 81 lines to infinigen/assets/tableware/chopsticks.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 0a568c05b94dd55c64aa5fbb9aae83dde506dca0 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 2 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit ca3873993033a3cf1f3a548f2c51df8a178b31e2 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 2 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 95ae48a23b24e9d20363b5a6257c4a6da7083970 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 67 lines to infinigen/assets/tableware/fruit_container.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 5457c0e1bfa25776823cb9a88f07bcc8b259cdac +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 1 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 95e480eaf59f540bcb3e92577fed83a869e06ed1 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 1 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit b8698a60415eb2e9a4322ad670c8a65bdeff7295 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 15 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 1020d08c520be75759994d1659c374a788cfd0bf +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 119 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 477b144ccfe5b3674084dd4aebc92c47b4025f9c +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 456 lines to infinigen/assets/material_assignments.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit 55cd1691f43e40ebf55ce9fbf642951be316ab38 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 1 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 0457e88065ef011d6ebf4902cd6a1c9fe0cc3cc9 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 4 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit e13509e7a79b0332c9f9c31b1b325411c5c0cdbd +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 93 lines to infinigen/assets/color_fits.py. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit ac8f7da7f668944a2b90703785f856de9085ebd6 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 173 lines to infinigen/core/placement/path_finding.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 27b21b60b3ccea2c833654aa6af960488d147a25 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 53 lines to infinigen/core/nodes/shader_utils.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 3663b5bdc766a8c8d95cc45423492318c5fd517d +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 79 lines to infinigen/core/util/bevelling.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 3d167d62fb02d4dd5981f12c1db7784bef21a83b +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 182 lines to infinigen/core/constraints/constraint_language/expression.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ff7845bbdca992eea0b83afcbf761fdfb18b7e52 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 2 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 3d6ed7dfa5ebc37da1b89dea1d376ecca4787d87 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 35 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 4fdc87a90e46154b5c1b01aad0da4cb8b71ba6c8 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 55 lines to infinigen/core/constraints/constraint_language/geometry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d958e95c7c9282e12eec8851240d0ab7c74e0be2 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 3 lines to infinigen/core/constraints/constraint_language/set_reasoning.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ca9ac168061b3f11478a181252ad9ac4ab3f13bb +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 80 lines to infinigen/core/constraints/constraint_language/set_reasoning.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit c18ac5cd6489eed7e649644c9b6029954f1dbe04 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 32 lines to infinigen/core/constraints/constraint_language/result.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 0caf21a9433956c4b1d91b446ac4e55aae5ddbe5 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 3 lines to infinigen/core/constraints/constraint_language/types.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ac681e51fef6068a7b1d6d1fe48215d237a724f8 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 42 lines to infinigen/core/constraints/constraint_language/types.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 4d7bf5ccfb12919b4ccf098b41633ab669ba2070 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 2 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 0b6840508f5df4c62a2e86d8888861e16c938bb6 +Author: pvl-bot +Date: Sun Jun 16 23:16:20 2024 -0700 + + Add 11 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit dbc84ef087c31f0d6b6c91a58839434b5c67b6f0 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 54 lines to infinigen/core/constraints/constraint_language/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 1d21848fd15d1a299ad9923bfc85e3303b2b6845 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 1 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 456641d7bbc095eb65dc21b55e91e8fa1fd44f38 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 3 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit f34257b82395078bd6065a9997a40e6c48aac08d +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 10 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 3ef3738a8dcee658ea17a6592707f87cd0449933 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 37 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit c918a1a7c576922e591fd31c0fb271cdd9079740 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 363 lines to infinigen/core/constraints/constraint_language/relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 26d01a01e3fed1ffb429ab658c99a2bd8dd438f4 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 4 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit dae03a8635a91fea9455b4000be91e50869e1e70 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 46 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d201928001b14fb1b31f2c3d028eae9410d627c8 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 354 lines to infinigen/core/constraints/constraint_language/util.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit bc6c0f62123d840ae3bf021e71a634a3f27dc1a4 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 1 lines to infinigen/core/constraints/constraint_language/gather.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 7ddb8bf1fc02f6b98f5ac343c2628e20cc7d6822 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 66 lines to infinigen/core/constraints/constraint_language/gather.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 9b1cf7e422b7d5f0a8764b6406f6f7a386fdc75c +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 1 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 61f37934754a4ead8ba877dd4f6ebf2c49e96ed0 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 3 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 32df7740ccac5613f22f73875a24dc9ba613c074 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 7 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit f35fc07c4dbad5c5e265f41415abdde273ebf685 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 68 lines to infinigen/core/constraints/example_solver/geometry/parse_scene.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 1ab422dbfb911a09e42e94c1893bc2a7f9313a41 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 7 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit bfbdf35a4c0dae51df44cf0a7a1d8fc79a46f759 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 10 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 88ea5301262d0994d8d81c556e3e2a9103f1bb6d +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 150 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ef8d79f4978cee56e20c9be9ceda3be7a5d97f79 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 259 lines to infinigen/core/constraints/example_solver/geometry/dof.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 916620ccace0ff797e9f0c9f617ec323e5ed8f68 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 8 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 6158f85e7cf94639f49817cc5ea6f90a060c46b2 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 27 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 52d7c9285526874fb414fca98720e9044a954c1d +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 268 lines to infinigen/core/constraints/example_solver/geometry/planes.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 583cfa63dfd002a3657f4e5afe80e96714a85d46 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 55 lines to infinigen/core/constraints/example_solver/geometry/validity.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 0e426d6ac5c64e4f08863610bf2b14c7a77a2916 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 67 lines to infinigen/core/constraints/example_solver/geometry/validity.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2d568a7216a3689492671c6d29add4fc001e1959 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 87c3cf5d47d14aa50f656f2a7e4c93fe2a88eda4 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 88 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit af67ce66529d5ca35b1d25acfca6e78467222660 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 266 lines to infinigen/core/constraints/example_solver/geometry/stability.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit c2a3b3fd7f012d4b36022b7f6a0fdbb8f0078fa0 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 1 lines to infinigen/core/constraints/example_solver/greedy/all_substitutions.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 9ecb38199f63d824bf3ad2cc9ce0a6911941b85d +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 175 lines to infinigen/core/constraints/example_solver/greedy/all_substitutions.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2b9f4e90f48147831f268120155a4b03341ec3bf +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 3 lines to infinigen/core/constraints/example_solver/greedy/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit b4dd499c70e776cb005df06afa6cced7ac0d29c9 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 269 lines to infinigen/core/constraints/example_solver/greedy/constraint_partition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit b48db72ebc9cbadc3e13496dba9259128e1d17cd +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 113 lines to infinigen/core/constraints/example_solver/greedy/active_for_stage.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 51db9cf3a26e6f09eaee5a939d2bf64bdb0d1a39 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 10ba901f8f74f2af858b25caff34c6c589b624d2 +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 25 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ddea8ba03e3a830cee88a409a97a9878d928514a +Author: pvl-bot +Date: Sun Jun 16 23:16:19 2024 -0700 + + Add 250 lines to infinigen/core/constraints/example_solver/room/scorer.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit bf0061463fe46732be27224c0482df7bee02da9c +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/room/contour.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit dd5145fb9a01b8dea38608ee3425e7912d751008 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 128 lines to infinigen/core/constraints/example_solver/room/contour.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 4fcfbfaa6789cb6280975241a092e2a6197f457a +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e1fef368f783b8ee3562f13f18c275a13c71bcf6 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 16a4b09351109d95b38b61e826ece1e648e18c04 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 134 lines to infinigen/core/constraints/example_solver/room/segment.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 0530bce30cf4bd41415c6c00df1fbd5951462dae +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 3 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 16825ed562b6b4b35725105a83633889ee644245 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 7 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit ace8689ac2bf5a3bb168a970b312d79acb3a529a +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 139 lines to infinigen/core/constraints/example_solver/room/utils.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit af2325601a5e42db7a19324126af72d5ef4d4df0 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 80 lines to infinigen/core/constraints/example_solver/room/types.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 0a2b8a3dfcf4b847aaca0369d22ce704f28f2fc2 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 5 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit c5b65d8aadc8427388cf6e74eb643ceebe606004 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 11 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 865e1a546bb959012bc30d39636b33c81112ba58 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 24 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ad83cb791cee28e44591e48109bfce537e5ba0b9 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 114 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ed4ba693dfaf0bff54824d67878b52f5f91b09c4 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 206 lines to infinigen/core/constraints/example_solver/room/decorate.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit c3f9a0e5a1b26c39b5ac418bd0e2c30aa848f449 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/__init__.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 25367eaf2e1069da9033dd41558fb58360470f61 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 11 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 543db110eea4a715a36ed4f949cdc7f9b7475581 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 15 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 583dd74e4fd7fa9a6fc630fb769123523e6c09bf +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 74 lines to infinigen/core/constraints/example_solver/room/constants.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 3fda728ff4c24f5b1e823cc7a6346a13a328f2bf +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 2 lines to infinigen/core/constraints/example_solver/room/graph.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 39bed180c090fdaca6a1a779e646f975d9f758b0 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 165 lines to infinigen/core/constraints/example_solver/room/graph.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 3e5ff0ccd2f2e1d419d548eb57db8a130000dbac +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 2f59ac5283a9b15d19af511c1f8faeef33a41ddf +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 9 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 63ad9b392d05fe6bfb2e62f2a1980bf4d0ef92ec +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 29 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 63eb43c822d0a34e45e53ac0fc995177cc6d81ef +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 174 lines to infinigen/core/constraints/example_solver/room/blueprint.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit da6e3f76c131b27a6635f72acaa5a2976776e7df +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 8d1fd1a4966375fdb05377b181a24cc24d80cfa2 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 6d74e65184ca2dda03605030f24d7fd9ebfb1774 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 273 lines to infinigen/core/constraints/example_solver/room/solver.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 97c20302917aee0f88879a3bd8f0e51f979e289d +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 16 lines to infinigen/core/constraints/example_solver/room/configs.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d455f742702d4eabe271fdd5d3a70ad8deb86d68 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 116 lines to infinigen/core/constraints/example_solver/room/configs.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 8632374545f5d688b6442d3649a4415df0912008 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 4ca8eaeef25c3858ae055c50ba98d849ed22b2f6 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 12 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ebf4d17d872ed6fc76318a160459abd51b9501fc +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 99 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 9f7e37e7cf85a981818dbc5d19296c4542357cfc +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 321 lines to infinigen/core/constraints/example_solver/room/solidifier.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit d81ace5d0dc717e43a4b4dc8b51f51928cb75616 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 52 lines to infinigen/core/constraints/example_solver/moves/deletion.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8e75f73d36c28eccc706eedb081f5e448005d231 +Author: pvl-bot +Date: Sun Jun 16 23:16:18 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/moves/pose.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 3808681f221f3d2ee91dd76f6f7037c9718cc792 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 116 lines to infinigen/core/constraints/example_solver/moves/pose.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit b138eed715cfafeadae881e23cda345fb4afdf47 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 5 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 7dcaea581871a50a0794aa969e7329ba5a7d8e81 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 15 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 0873d0169cfc9738cccd5db487e9e45043f0a0d8 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 168 lines to infinigen/core/constraints/example_solver/moves/addition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 25fbdff205a763aa8cf6b4fc8d59d76f43871066 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/moves/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 1761c4421d95fd6a3015399c1571686e7894b6ce +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 62 lines to infinigen/core/constraints/example_solver/moves/swap.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 5e533523032ace58b07df14e3e3044f3a6fab335 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/moves/reassignment.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 97ba36b8a1384a3f4e9c72e82993373de0873e22 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 106 lines to infinigen/core/constraints/example_solver/moves/reassignment.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 0f011dcc7187162c8904ca95e3346726674aa566 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 37 lines to infinigen/core/constraints/example_solver/moves/moves.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 593bfbb6dc2db247efb7a896f1c70efd878a2dc1 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/populate.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit a782b61f7527bf10243d93bb91920fc7cc76c340 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 114 lines to infinigen/core/constraints/example_solver/populate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit b347420d27ab4d491d0bf5fc73471196371e3fd3 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 4 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit ef9be11101b2ff415f74cc2d59c3945ec09e4425 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 6 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 1ff89f29a71008c90bd83f1ae240d22986f878a1 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 8 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 31ae234624d32ef8eca1f89270dadda3f1e6d95f +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 8 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 557c0b3cb021a49a31dac7ad6206901fb4b5ad5e +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 216 lines to infinigen/core/constraints/example_solver/solve.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 973168125e36524d08f3b3698b089fd3e0897165 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 148 lines to infinigen/core/constraints/example_solver/propose_continous.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit c7814a710cfb43fc44067d757ad7aa2529f1c8c2 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 5 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 69ef24df083a866401e5882984da9ce2d405dbaa +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 12 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 39eb45fb980ff9ce23b8b8233a1a9aa77b9b8c99 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 333 lines to infinigen/core/constraints/example_solver/propose_discrete.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 022a462ba8423da64ecdb31509a7f0693a43ac4d +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 2 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 2b2a2a8908ad5be22cab890f9725ab99d651580b +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 7 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 50d9ffce021d424607d38a0a6be61efab5b7e58c +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 9 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 1f7e3022a0b786f6a31d5d7c38d033efbef5b578 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 17 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 1d17d97b8b356e2681746efd06c934c33967fd79 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 172 lines to infinigen/core/constraints/example_solver/state_def.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit c95abae83707ef96f7a4eb5a0714d10bf7fc937b +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 3 lines to infinigen/core/constraints/example_solver/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 677077fdeda5df24773f72cbee2c240282e70ee6 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 151 lines to infinigen/core/constraints/example_solver/propose_relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit f4fbb450d29abf9ae6df0d82fac5ea41275cf443 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 7 lines to infinigen/core/constraints/example_solver/annealing.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 7362439725681f36134812843fada28cd96e9945 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 365 lines to infinigen/core/constraints/example_solver/annealing.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e001ff8e3c5c63b4f7c513eecb9378b4d085cb40 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 66 lines to infinigen/core/constraints/reasoning/domain_substitute.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit df4f36249cda10ba79743c4d0a31ee22136cb1f4 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 21 lines to infinigen/core/constraints/reasoning/constraint_constancy.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2365e12080add0ee8abe934b1ac36586236a64c6 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 22 lines to infinigen/core/constraints/reasoning/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 61eed6d6efcf19058ad0e9b870472c7050a1e6c6 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 1 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit b14e66eb75e032ce514a126b6014983dda41a80a +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 35 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 3419d1bf0431941075dc9f00b37674c9aa576963 +Author: pvl-bot +Date: Sun Jun 16 23:16:17 2024 -0700 + + Add 174 lines to infinigen/core/constraints/reasoning/constraint_bounding.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2d1acf44a3b263f7d77a63a904a0d4d9d6faaf8d +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 1 lines to infinigen/core/constraints/reasoning/constraint_domain.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 0aa2c390433233d3e71f349eff4bef08945bbd89 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 74 lines to infinigen/core/constraints/reasoning/constraint_domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit cd88dd267a99b53daef2dd345756378754d87c4b +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 416 lines to infinigen/core/constraints/reasoning/domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 4098d2de731f4e49dc475b620efbca10063d186e +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 64 lines to infinigen/core/constraints/reasoning/expr_equal.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8769347e49682e70da29f51bde06408193555952 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 3 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit a733a20254dfadb7d1a28cda189b6520245cb46c +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 42 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 21401795ce7981746fc11ed844be21a3adf7e1b5 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 52 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit ad7d9faf1c58defa8edd2f0be30ebead50c51095 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 277 lines to infinigen/core/constraints/evaluator/node_impl/impl_bindings.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 9b8e3674ca870e62322a669c6487a0a3851a005f +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 1 lines to infinigen/core/constraints/evaluator/node_impl/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit dac544843a5c7206e05a80a9a46538b86ca39881 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 9 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 33666d75b7b0da783702f12c7a34f0bd347bba96 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 170 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e92b795126b13f3d909a529b1f5519eb7894e289 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 993 lines to infinigen/core/constraints/evaluator/node_impl/trimesh_geometry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 8ae175b7bd2e0e53c39df347e2f051e4c7eec7d2 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 1 lines to infinigen/core/constraints/evaluator/node_impl/symmetry.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e6ec3d3cef708147e1161cff12e0829b2f9944a4 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 262 lines to infinigen/core/constraints/evaluator/node_impl/symmetry.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 5b26f6c7497a7258a7cf15ab63f909047a48f9f9 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 5 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 09e2990ed80fd498db440415359e933702f2404d +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 26 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 9bb4d4bb9633d8795fc0c5ea059fdb5bf19d4c20 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 224 lines to infinigen/core/constraints/evaluator/evaluate.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 08afd13cf72f6512988e496bdfea27417c552458 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 231 lines to infinigen/core/constraints/evaluator/indoor_util.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 3ab967494e9146aa2f90c0ad51c85a6bdd8461e7 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 115 lines to infinigen/core/constraints/evaluator/eval_memo.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 128ed562848dd7f178d032df9a445ce6a13ae858 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 10 lines to infinigen/core/constraints/evaluator/__init__.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 6d50d8daf81b530eef2037c0f7468644b71778d6 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 1 lines to infinigen/core/constraints/evaluator/domain_contains.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 6512c8d39b0d76f7f6ea136d971d638305f3c262 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 59 lines to infinigen/core/constraints/evaluator/domain_contains.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ad642910695461d192beaaa47a6ce04ed3a6fa08 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 126 lines to infinigen/core/constraints/checks.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2dc5fec21b78d1288568649f82f4e52d69fefa82 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 3 lines to infinigen/core/constraints/usage_lookup.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit dd7dd7b90e96a16f835d37f71ab924f2e637e9f1 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 45 lines to infinigen/core/constraints/usage_lookup.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 7d8a4772287b04e352f8a9da16f09a6831a36ddb +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 9 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 92d4eb9d92908b51ba472572bcd5900ddbd94558 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 19 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 9023e340b9fb925c4c2ee2c7c30dfbd20e2ce03f +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 25 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 9842170aeb3590081273d34a484c3778d5434636 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 30 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Lahav Lipson. + +commit cb923fe808235484edfb263e6329b5f1a6dfb5d4 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 345 lines to infinigen/core/tagging.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ef8e50d19d46ebb108e367069ea5d25a07509218 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 3 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit f69c57cf10cb89db286bf531484852c40f8394be +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 7 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit dd9c78879a011f8d260e9462c2c0ad2fb5ed1bec +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 80 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit a9c72789e0e44f13929c7b64a667c6c549f4be31 +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 215 lines to infinigen/core/tags.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d4331267447675006a16589e668fb53540784c3a +Author: pvl-bot +Date: Sun Jun 16 23:16:16 2024 -0700 + + Add 1 lines to infinigen/tools/results/visualize_planar_graph.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 6c3a0045a1d697244c987ab51d9f1fa2dcb16330 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 70 lines to infinigen/tools/results/visualize_planar_graph.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 0a4ef0e03a609e4b4b56c306126f0bc2fc899b3c +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 12 lines to infinigen/tools/config/demo_config.yaml. Contributed as part of Infinigen-Indoors by David Yan. + +commit e823bcbd04b5579744253bcbc05bb5ccbd34e78c +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 37 lines to infinigen/tools/perceptual/create_submission.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 3b360a665f80a6e66b14c798d15b41ed83bfe6b1 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 411 lines to infinigen/tools/perceptual/analysis.ipynb. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit b47966ce5cc1ec88232b9b566048c488fa834823 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 34 lines to infinigen/tools/perceptual/perceptual_extract.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 3262add86904d8f21f31365c54055ff3839b11fd +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 12 lines to infinigen/tools/perceptual/rename.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 65a5a561b822f224e79b03d1fb76bf63450bc310 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 13 lines to infinigen/tools/perceptual/rename.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit dc15981b2a1cea10bcea46baf540675008996ccf +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 187 lines to infinigen/tools/perceptual/create_pairs.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 4b94c4073e6493683f2a8760fd503b15ba38a355 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 124 lines to infinigen/tools/convert_displacement.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 757024b09ce3cd182b64b8adcec9da02beffb638 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 32 lines to infinigen/tools/isaac_sim.py. Contributed as part of Infinigen-Indoors by Beining Han. + +commit 36b10aa2d5f157746a58eab317d0f04ef5cc242e +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 139 lines to infinigen/tools/isaac_sim.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 9bed07285779baa685d0446a0ecbf4f003e85ce9 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 83 lines to infinigen/tools/indoor_profile.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 2b09762b5cadff70822cf8d226d600c991e9c276 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 63 lines to scripts/eevee_render.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 618739d0000fb680f8ac74b3d2009d6e8ddb0de5 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 11 lines to scripts/rebuttal.sh. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 2705f90c1b22e3b4ad340c18f4c52a3434f5b1d4 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 72 lines to scripts/rebuttal.sh. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 4852bbac5f3d23fa41b9afd852c796ff93b58f8e +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 8 lines to scripts/indoor.sh. Contributed as part of Infinigen-Indoors by Beining Han. + +commit 3696c69adefd7551b4dc05ef618e39e0976b5132 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 36 lines to scripts/rebuttal_retry_render.sh. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 62a1a4fec2ff9feb7ad64781a7e6cae330b2386d +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 2 lines to docs/HelloRoom.md. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 95533f59846161e86190fd2ebf81c70e0736d870 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 125 lines to docs/HelloRoom.md. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 3160f6188c5cbe8b5d339207a0ca92c699da2de6 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to docs/ExportingToSimulators.md. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 699f3ae821fd3687068ea11833ad9cd5c68d8959 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 32 lines to docs/ExportingToSimulators.md. Contributed as part of Infinigen-Indoors by David Yan. + +commit 9f486f85ff5bd58c20d51c068c4382ae0e19986b +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 6e20cbe65894225c5767da3a080e38df01a8d3bd +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 05cd77cffd7678711516111f8b2f887693cb2e47 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit a1bfe6d288def89442a667957e0dce7c08c6fe51 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 3 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Meenal Parakh. + +commit ac5cb276f32fc01a36bdfb9201165da86c359f6a +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 12 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 36ee4ea749d40aa56ceb2b81743e61e32297ba14 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 17 lines to tests/assets/list_indoor_materials.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 877e6688541fe6932124ff8d621dfe26f69dfaf2 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 7 lines to tests/assets/list_displaced_materials.txt. Contributed as part of Infinigen-Indoors by David Yan. + +commit fdf533d6bb65cbaece0a6b813193965b4a17205e +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 7 lines to tests/assets/test_materials_basic.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 125472b563eb95b76a7ccdc06539cc3c1d0e40ec +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 40 lines to tests/assets/test_materials_basic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8da48f0f045bef1a9f85c1d9ed9ef9eafc8376c8 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit b15a70034bf2688c53ed38085e7dbc0b8ae49640 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 2082175925a9365625e5bbd089446456a6043d05 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 1 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Beining Han. + +commit 6db1fbcd3f0b658dbbfb81d2a512805b428f982f +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 3 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit e811301b630d04ab5779b31691dd63e8787e7fbe +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 28 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit b8e0d257ae54e6a6dd12aa0c5e358a0aa42597f1 +Author: pvl-bot +Date: Sun Jun 16 23:16:15 2024 -0700 + + Add 66 lines to tests/assets/list_indoor_meshes.txt. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit aecacaa0f695c4f5dde9b4932c2f7af9a306c4bf +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 1 lines to tests/assets/test_meshes_basic.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 38db10ef300d9dd30344fca1c972038a59cdc10e +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 89 lines to tests/assets/test_meshes_basic.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit a6c9bfe47d85fb65fac8ea2ef02befb3fbaa6610 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 4 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 94f7d2b0044462d5091eb8aaada511a9ecb2f482 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 7 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8d051a01c0446760ffd9d84a0b36ef8411307972 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 57 lines to tests/assets/test_placeholders.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 9bbb94aedab2b9d80a99575b380eee26a1b9468f +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 21 lines to tests/solver/test_greedy_stages.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 48d39929f807b7b9fe9161ff57a5c904defedc28 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 286 lines to tests/solver/test_greedy_stages.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit cd6d04fc548b5507328c506220d999f181a74e19 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 44 lines to tests/solver/test_state_def.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 0e2fe0f9f6feead7ee9fb60669a5fe2d2b1fd30d +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 120 lines to tests/solver/test_greedy_substitutions.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 202e3c39073f21064b02c12f2353815f766d98b2 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 471 lines to tests/solver/test_greedy_partition.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 852cfa452f0e4a8b25e7756cdc0ea408176618b0 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 5 lines to tests/solver/test_asset_surfaces.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 5f032e1af206c1c3ae9cc5518e865f42838ea8b0 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 56 lines to tests/solver/test_asset_surfaces.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit c1f6ced9ef5d8853a248e45526985f7c926f05d7 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 83 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit e0b4e342488190622fc1ad5bbfeaa4ff73cc52a1 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 218 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit f080965f5e1af641872fb2bde4d7229d7b50bb1f +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 679 lines to tests/solver/test_constraint_evaluator.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 6baf8e5fb4f0683e9c6ef185163336db9d050785 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 6 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 662dbf04c3915cdfec063ae05aa5b9f3ecd10cdc +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 74 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ebc6fdcca9cbd9b7173dd3d3f7f10421f81247ec +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 84 lines to tests/solver/test_stable_against.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 22b6f554ff6cb02ac63a55d131dea5842b51a3ed +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 29 lines to tests/constraints/test_constraint_domain.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 71b77e1fb2848a82ccc6befafbb107f28471d5c3 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 170 lines to tests/constraints/test_constraint_domain.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8e1ea377cc33439e2394a36f68170c1807b29e3e +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 13 lines to tests/constraints/test_constraint_relations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 2c13da8ddfa26450038aefe01ddbc82095df6d4f +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 83 lines to tests/constraints/test_constraint_relations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 8e3e746291e9d206be336e80b392a4b94831d242 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 77 lines to tests/constraints/test_reldom.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 0c723cdc2bc2c2deba9a01c21f22697c7153fc85 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 18 lines to tests/constraints/test_tagset_operations.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 35b393f202154e59710ed4f8ebf778c2229e2e01 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 25 lines to tests/constraints/test_tagset_operations.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 7dc61ce5765e74391129836829bf99c4526e8e2e +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 15 lines to tests/constraints/test_tags.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 33b8f9d33f4af56a0059a0ef4897ab69c38306fa +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 43 lines to tests/constraints/test_constraint_language.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 992a1a7c2d3ed371fdd5681c7391868f6aa2b843 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 10 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 49df4a176209f3ebe004adbffae8f632684381f4 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 27 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ee2950ffc77b28dfeb3ee25be16ae76459203314 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 154 lines to tests/constraints/test_constraint_bounding.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 9b03e4165f17be3e0bfa1675ead1980c9157aaaa +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 15 lines to tests/core/test_tagging.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 5318cf79dda4173a7c982720bab2993fc1692173 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 82 lines to tests/core/test_tagging.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d01a5d270c151af34ba440f7341b0f770fcea207 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 39 lines to tests/core/test_gins.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e6dd3c610de11316ac90e72c487692893f5a5a15 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 28 lines to tests/tools/test_export.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 843d90397d39b2e494500b8327d8c5c4b7dd2a7c +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 52 lines to tests/tools/test_export.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit aed7c77b255edc60065effa0cf9eb08cbb17f022 +Author: pvl-bot +Date: Sun Jun 16 23:16:14 2024 -0700 + + Add 7 lines to tests/list_displaced_materials.txt. Contributed as part of Infinigen-Indoors by David Yan. + +commit 7a93f7205e08c2dbbaf7bd6d7f4270fb43e3e5de +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 35 lines to tests/material_balls.txt. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 13f280963796bb7207651997d1f53acff9fa2d49 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 3 lines to infinigen_examples/configs_indoor/disable/no_objects.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 428c51532435454b2c5c154cc7f11cd91d1d395e +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/configs_indoor/disable/no_details.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d2fb7a324f5187906cbc9c1a064dad866cca5345 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 7 lines to infinigen_examples/configs_indoor/disable/no_details.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 88bf8ec0ea810ba2d36ef6e85a94c1996841a8a1 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/configs_indoor/studio.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit dbd9d60f7a2b4e70fe41ae940c6087565479fb3a +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 7 lines to infinigen_examples/configs_indoor/multistory.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 82d9f85836625a41e6daf71a0e06a77f5149fade +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 7 lines to infinigen_examples/configs_indoor/multistory.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit d0531a71f35682af71fe08839ac2e98af444e4f5 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 7 lines to infinigen_examples/configs_indoor/overhead.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit b8d11d80341482b5feb857933df993ccbdb8a34d +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 8 lines to infinigen_examples/configs_indoor/overhead.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 8ff32f1b8f2bb865db6eab51f3ba9967af487227 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 19 lines to infinigen_examples/configs_indoor/fast_solve.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 82ceea367391f619e680eebfeae5b32870d506bd +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 4 lines to infinigen_examples/configs_indoor/topview.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 8d9ba331e5c2054f874a9129988070bc8280220e +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 4 lines to infinigen_examples/configs_indoor/topview.gin. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 3d403c4644bcf455ea157ae494a4b18869031b80 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/configs_indoor/export_upload.gin. Contributed as part of Infinigen-Indoors by David Yan. + +commit 7b89026662f8161f5da8c99d6d58bc8b32d5ea0b +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/configs_indoor/singleroom.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d9be42ac856eb1f0ae977cdddcb8021191aab744 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 478badcddee79412359dd81d166a1ba7afa3c5d1 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 6 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit 2717fe91df5be267d07fd2efe689364c8982a33a +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 58 lines to infinigen_examples/configs_indoor/base.gin. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit bf64297829c4da6e7f86c6ac7911a58ef07560a3 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/util/constraint_util.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 737265cf42087b551f2133e7a6a14cc36a751c11 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 62 lines to infinigen_examples/util/constraint_util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit ccb851d7cf43fa769ce8cc33ab9b2687f6b122c9 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit a1c4834c601099c572b5ccbaec69b4605e13f2e2 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 5 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit f4ba54c94235d9aa715a491cec9dd2d4ebd6a0ab +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 258 lines to infinigen_examples/util/generate_indoors_util.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 3a7e3a7750a01cb4db3409e9c4ba624c3525e419 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit c6cdfa71852761ddbcd30332c7af12258b165ddb +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 16711191bc293f451ad0561a37877340ea2bc7d2 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 57 lines to infinigen_examples/util/test_utils.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d01430919464dd048c3cae264ef7a30da737c8b4 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit f053a032b24963a46e69561caaebb8c0516f2de0 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 1 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit a7f5c07e33c48f3894fb7e6b7eed96ef2b5a45b4 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit b03147cd6fff0d0e76f4cd82b4963be3728f15d5 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 2 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit b97b816b14df8ed6ea73cb087b52166373d9332f +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 19 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 6014f883ac56ba254ad39909db0f0cd1a2a6e1ff +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 865 lines to infinigen_examples/indoor_constraint_examples.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 6c789c103d9cff2491973cbafd0dd5ad5df91322 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 203 lines to infinigen_examples/generate_material_balls.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit e46952295de3b90d5aa0aaf58452c4fd17ce5a39 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 6 lines to infinigen_examples/indoor_asset_semantics.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 3bd77582713236871d15fcfcaf3f880308f39c82 +Author: pvl-bot +Date: Sun Jun 16 23:16:13 2024 -0700 + + Add 332 lines to infinigen_examples/indoor_asset_semantics.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit e46aefc5bee3d3979609803ca30430e2ee9bf3f0 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 45 lines to infinigen_examples/asset_parameters.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit f4eeb4bfe1272599af4e16c1c3483a4aaae7a3de +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Stamatis Alexandropoulos. + +commit 9bca4587dff99a094a4c16047fb18207ebdc3dbe +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Yiming Zuo. + +commit 24b000109867afe807c832393bccd5c5a5902182 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 1 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by David Yan. + +commit 56b88a113f79a0e2091304375f857a3c3bdbf93b +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 3 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Karhan Kaan Kayan. + +commit 9bb58746a6391a0104b0012e2c80096da51b87eb +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 12 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Pvl Bot. + +commit ba87ded72811420f12918238a7684481d091c2e0 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 14 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Zeyu Ma. + +commit 1b5ffe310d7ff6cc2bfbde6306aa7c3bd7834a5d +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 38 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit d33eb1b151fb23562266b4cbe8a9ce6a41262896 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 356 lines to infinigen_examples/generate_indoors.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit 22b9acfbfa7d47c985ee1027428c4e81f690f3c2 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 1 lines to infinigen_examples/generate_asset_parameters.py. Contributed as part of Infinigen-Indoors by Alexander Raistrick. + +commit d1268dfd23aec66495f90bd11a72e3846ff8b0f9 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add 328 lines to infinigen_examples/generate_asset_parameters.py. Contributed as part of Infinigen-Indoors by Lingjie Mei. + +commit 356dc9c98404f24bb06501c33a974f6422e27b95 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Improvements to exporting.py, contributed by David Yan as part of Infinigen Indoors + +commit b3fa5e9471c29b0cdef00206aa9863a6bf4223e4 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Changes to existing files contributed as part of Infinigen-Indoors. Contributed by all authors - we were unable to create correct per-author commits for edits to existing files. + +commit 001797f5419dc02fe6e39a10e7e496fe907b49f3 +Author: pvl-bot +Date: Sun Jun 16 23:16:12 2024 -0700 + + Add files that were renamed/deleted as part of Infinigen Indoors + +commit f98b9575c73c322f1cbcc2dd48d26bbe8d6239f6 +Author: Alexander Raistrick +Date: Sun May 26 13:16:44 2024 -0400 + + Fix typos in export documentation + +commit f5bcba8de47623da9b715348cf95822445e0e9a7 +Merge: 830891c3a 857c9be95 +Author: Alex Raistrick +Date: Sun May 26 12:29:33 2024 -0400 + + Merge pull request #245 from princeton-vl/rc_1.3.3 + + Merge v1.3.1 - v1.3.3 + +commit 857c9be957fa14a40a75371a2aa5b64aecedc21f +Author: Alexander Raistrick +Date: Sat May 25 19:01:59 2024 -0400 + + Fix unit-test failures for empty materials + +commit e0a6e254c11a920e078867a1b488e4e68102d8d3 +Author: Alexander Raistrick +Date: Sat May 25 17:29:35 2024 -0400 + + v1.3.3 - Camera bugfix, pass all tests + +commit 03f603d030f3f8571808b96180514149c11a64fa +Author: Alexander Raistrick +Date: Sat May 25 17:08:27 2024 -0400 + + Add purge_empty_materials to every geonodes apply to fix emptymat tests + +commit 113230a056826d07e8de2a24f7087975ef85c48d +Author: Alexander Raistrick +Date: Sat May 25 17:08:07 2024 -0400 + + Add pytest timeout, reorganize tests/test_meshes_basic.txt to pass + +commit 7e3d6b31321a5f651a63eb615b442e7423b0eb03 +Author: Alexander Raistrick +Date: Tue Apr 23 13:49:06 2024 -0400 + + revert fps change + +commit dfc2b2ab1a8df700575f6d518685932c26ce396a +Author: Alexander Raistrick +Date: Tue Mar 5 13:47:31 2024 -0500 + + Add METAL to devices list + +commit 7d821161cfa6aac2e8c1ad317784b5e38bdb410e +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Tue Mar 5 11:00:04 2024 -0500 + + decrease sss radius + +commit 7538de1b97846fd3eab729f213b79b8768e13a42 +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Tue Mar 5 10:57:38 2024 -0500 + + multi cam fix + + * fix + + * fix placement + +commit 3078b0cfa02c2bd35877ba9aaec300b8125fe1c9 +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Sun Feb 18 14:21:06 2024 -0500 + + refactor camera selection syntax and prevent water in all frames + +commit 3c96b3b99c6e00413a3a125804c18b2e455d63f6 +Author: Alexander Raistrick +Date: Sun May 5 14:58:33 2024 -0400 + + Bugfix snakes & manage_jobs (v1.3.2) + + * Move glowingrocks to rocks to prevent circular/far-reaching imports + + * Update OcMesher + + * Fix white materials on snakes, add unit test for empty materials on any asseet + + * Fix denoising always on + + * Add slurm_partition to manage_jobs + + * Update formats_all, make release.py work with just a folder of videos + +commit 35506e8821830bd34af04eaef44a28f9f6932d8b +Author: Alex Raistrick +Date: Mon Feb 12 12:45:37 2024 -0500 + + Smbclient commandline utility + + * Add commandline bindings to smbclient.py + + * Add mapfunc for slurm launch of smb_client, WIP fix ls with glob at end + + * Fix recursive glob ls and download + + * Add exclude flag to eliminate downloading blends + + * Add verbose, ratelimit + +commit dd5d560cf0495b51b5991d9a8788d6a5ee4e7e7f +Author: DavidYan-1 +Date: Sun Feb 4 15:37:34 2024 -0500 + + Texture-baked exporting (princeton-vl/infinigen_internal/#103) + + * Full Scene Exporter + + * Regex Tweaks for Integration Testing + + * Refactor and optimize export + + * Move exporting README to docs/ folder and add more caveats + + * Path handling tweaks for exporter + + * Move export to infinigen.tools.export + + * Tweak docs + + * Add slurm scheduling to generate_individual_assets + + * Tweak generate_individual_assets + + * Small Refactor + + * Glass Export and other features + + Glass, Individual object, .obj vertex col export and fixes UV overrwrite + + * Add --export option to generate_individual_assets, use a slurm job array per factory not for the whole set + + * Add docs on generating & exporting individual assets + + * Export Optimizations and Bugfixes + + * Remove Unused Args + + * Typo / import fixes + + * Add --export option to generate_individual_assets, use a slurm job array per factory not for the whole set + + * Fix kwargs + + --------- + + Co-authored-by: Alexander Raistrick + +commit e4a42d7cf9084c271f908070c559395747ec27fb +Author: Alex Raistrick +Date: Wed Feb 7 09:12:11 2024 -0500 + + Render improvements (v1.3.1) (princeton_vl/infinigen_internal/#107) + + * Attempt to fix white snakes and volume_bounces=0 bugs + + * Make videos 4sec not 8, tweak slurm_1h, tweak terrain resolution, speed up cameras for video + + * Fix --use_existing for scenes started with --specific_seed + + * Increase opengl time, decrease render sample quality + + * Noisify camera motions + + * Add overhead.gin + + * Tweak video length and cam seped + + * Drop fps to 16 + + * Reorganize cycles configuration, overhaul enable_gpu to be more robust and only ever use one device type + + * Bump version to 1.3.1, remove scripts/launch + + * Tweak ratios, fix typo + + * Fix noshortrender typo, rename conf to noisy_video + + * Changelog + +commit 830891c3ac988f6be02ded26388b1572a5463de3 +Merge: 18be26c9b 80ac24c68 +Author: Alex Raistrick +Date: Wed Apr 24 22:34:04 2024 -0400 + + Merge pull request #122 from princeton-vl/rc_1.2.6 + + Bugfix v1.2.6 - Fix duplicate configs, CUDA_VISIBLE_DEVICES & more + +commit 80ac24c681ace3f811d1eec7363f53f4d3725ae3 +Author: Alexander Raistrick +Date: Wed Apr 24 13:22:09 2024 -0400 + + Increment version & changelog for v1.2.6 + +commit 4216ac371f3caab32666a0e3cc79f75ab0afe00f +Author: Alexander Raistrick +Date: Wed Apr 24 15:03:31 2024 -0400 + + Unify other creature interfaces + +commit 069ad8b12f932d44619928d4d54c08a85c5f9235 +Author: Alexander Raistrick +Date: Wed Apr 24 13:07:40 2024 -0400 + + Attempt to fix dynamic hair for #215, raise NotImplementedError for dynamic hair for now + +commit e7b3ef04df314b62e94047fa30321dbd48c054aa +Author: Alexander Raistrick +Date: Wed Apr 24 11:52:41 2024 -0400 + + Fix submitit emulator improperly following CUDA_VISIBLE_DEVICES (#212) + +commit 7177237a64ad00bddb32fd81e516ef7ef37d3d29 +Author: Alexander Raistrick +Date: Wed Apr 24 11:22:05 2024 -0400 + + Add trycatch for flowvis install + +commit 43244c9e26c6498182a1128cdf8f45115a467bba +Author: Alexander Raistrick +Date: Wed Apr 24 00:19:44 2024 -0400 + + Avoid duplicated configs by skipping sample_scene_spec when --configs specifies an option: + +commit f54f536420a759e56da3b4a11af9ed10806c4d1e +Author: Alexander Raistrick +Date: Tue Apr 23 23:34:27 2024 -0400 + + Implement mutually exclusive folders for scene_types to prevent common error case + +commit 18be26c9b4a7b375442d23569b737d8e2169e372 +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Tue Apr 2 11:16:34 2024 -0700 + + Bugfix v1.2.5 - Terrain bugfix for multi-task command; Terrain.bounds and Terrain.populated_bounds parameter + + * fix reinitializing terrain + + * add bounds and populated_bounds for terrain + +commit 5132903cd68704367d1c44c841e5163158e0f33d +Author: Alex Raistrick +Date: Sat Mar 9 16:08:10 2024 -0500 + + Update Installation.md + +commit 331b4e5e8b3d84f8a31f395c8a344df20844195e +Author: Alexander Raistrick +Date: Tue Mar 5 15:24:17 2024 -0500 + + Hotfix v1.2.4: Fix TreeFactory(season='winter'), fix join_objects ignoring empty meshes + +commit 66a449399f2f38d63e4b9aad689c02a4479f14df +Author: Zhangir Azerbayev <59542043+zhangir-azerbayev@users.noreply.github.com> +Date: Wed Feb 28 15:51:32 2024 -0500 + + Update Installation.md (#192) + +commit e8687f4ab5e809be28778fe42e26d26b414cca6a +Author: Alex Raistrick +Date: Mon Jan 15 13:44:40 2024 -0500 + + Update Installation.md + +commit 40a261506252f17107ff7398d787a3feb13a53d1 +Author: Alex Raistrick +Date: Fri Dec 29 09:49:57 2023 -0500 + + Hotfix v1.2.3 + + * Fix misplaced opengl path, tweak installation + + * Tweak launch scripts + + * Fix underspecified child python paths + + * Fix cleanup except_crashed + + * Fix underspecified child python paths + + * Remove cd worldgen typo + + * Fix leftover objects in tree generation + + * Change version to 1.2.3 not 1.2.0.3 + +commit 0c622a1788235e2270a73262463383ff1fade70a +Merge: 48be1cde4 e23073612 +Author: Alex Raistrick +Date: Sun Dec 17 15:24:59 2023 -0500 + + Merge pull request #184 from princeton-vl/hotfix_v1.2.0.2 + + Fix hello world crash + +commit e23073612e4d953d0c3a6f3639262e566a69ab9a +Author: Alexander Raistrick +Date: Sun Dec 17 13:45:58 2023 -0500 + + Fix hello world crash + +commit 48be1cde4f44e79b237fdf0170fbf28084f80704 +Merge: 873fedd61 ff086b6cb +Author: Alex Raistrick +Date: Sat Dec 16 13:27:44 2023 -0500 + + Merge pull request #182 from princeton-vl/hotfix_v1.2.0.1 + + Hotfix helloworld, data download, smooth shading + +commit ff086b6cb6c19b99bf01f1ddf53a0c156df4b720 +Author: Alexander Raistrick +Date: Sat Dec 16 12:36:17 2023 -0500 + + Hotfix helloworld, data download, smooth shading + +commit 873fedd6108e356c701112c8e67f300e4fe69d02 +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Thu Dec 14 23:40:11 2023 -0500 + + Update .gitmodules + +commit fcd7fc265e5a7c25b56e431e4ed57c3b5320fb42 +Author: Zeyu Ma +Date: Tue Dec 12 14:35:23 2023 -0500 + + Integrate OcMesher + +commit e4765d06e4794e45e166066be9a12fe188155285 +Author: pvl-bot +Date: Tue Dec 12 14:22:19 2023 -0500 + + Fix typos + +commit a31950f9ada0345fbf7812e78ad0f5598d170ff1 +Author: Alexander Raistrick +Date: Tue Dec 5 13:26:52 2023 -0500 + + Remove build/publish gh actions for now + +commit 998ba6bf657cd99710a8a17d1e2cf57c60b9e9a8 +Author: Alexander Raistrick +Date: Wed Nov 29 17:16:20 2023 -0500 + + Fix snakes, import crash, CI failures + + * Fix snake material, logging bug, fix debug crash + + * Add trycatches to prevent marching_cubes crash when not installed + + * Fix linting + + * Update changelog + +commit 71f254a7b31d2abc4ff02388a65d9aca5ca068e7 +Merge: e9313a618 04d8e573f +Author: Alexander Raistrick +Date: Tue Nov 28 15:48:14 2023 -0500 + + Merge branch 'main' into rc_1.1.1 + +commit 04d8e573f68b95981cc206ce858126bec0c5a57d +Author: pvl-bot <136786582+pvl-bot@users.noreply.github.com> +Date: Fri Nov 24 13:29:53 2023 -0500 + + Update dependencies, loosen version reqs, remove unused dependencies + +commit e9313a618220bfe02bc2b90f863143c64ac67687 +Author: pvl-bot +Date: Wed Oct 25 21:23:34 2023 -0400 + + Transpiler v2.6.5 - revert Ignore Reroutes to fix incorrect linkage bug + +commit 24242098961519c0678ff3757d4a9736358c4778 +Author: Alexander Raistrick +Date: Mon Oct 23 21:23:57 2023 -0400 + + Add infinigen.launch_blender helper, remove $BLENDER, fix test, update docs + +commit 2f025c5ff055b2538080fbceba36fb76e7da5a91 +Author: Alexander Raistrick +Date: Mon Oct 16 23:45:03 2023 -0400 + + WIP revised install instructions + +commit 091d6ec5ba61cab9db97851ccd92f3fdce005dd7 +Merge: ef75fe7fa a8ba86a39 +Author: Alexander Raistrick +Date: Mon Oct 16 17:01:07 2023 -0400 + + Merge branch 'main' into rc_1.1.1 + +commit a8ba86a394f8757586b4fb15252a3282e56a91de +Merge: 0f208a804 359f08e7c +Author: Alex Raistrick +Date: Mon Oct 16 15:58:16 2023 -0400 + + Merge pull request #87 from princeton-vl/rc_1.0.4 + + v1.0.4 - Pregenerated download tools, ground truth updates, render throughput improvements + +commit 359f08e7cfdf6caa3acf84bbb1bdaf6fbfe9a91d +Author: Lahav Lipson +Date: Mon Oct 16 15:44:15 2023 -0400 + + Update docs and download_pregenerated_data.py (princeton-vl/infinigen_internal/#90) + + * Update docs and download_pregenerated_data.py + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Fix hello_world example + + --------- + + Co-authored-by: Alexander Raistrick + +commit a646f3513d5512911fb5488537a8a724b57b79f3 +Author: Alexander Raistrick +Date: Sun Oct 15 18:54:25 2023 -0400 + + Make download script interactive, revise data docs + +commit 02cd5b6045b22ec772706257d0722d2f6b13b57f +Author: Lahav Lipson +Date: Sun Oct 15 17:35:19 2023 -0400 + + Refactor bounding_boxes_3d.py segmentation_lookup.py (#89) + +commit 79244928803532393f7f572c6f03540c0d9d1138 +Author: Alexander Raistrick +Date: Sun Oct 15 14:35:00 2023 -0400 + + Debug render pipeline getting stuck due to backlog limiters + +commit ef75fe7fa1d9155f365e0ac8344d6fe35d38f9b0 +Author: Alexander Raistrick +Date: Sat Oct 14 13:07:00 2023 -0400 + + Fix pip install with cuda, build.yml, terrain test, mark which tests are ci + +commit 6780f819cc2ba25e9298148ce35d0aa9ec02195c +Merge: f7889fb3c 3664df8bf +Author: pvl-bot +Date: Sun Oct 15 00:09:21 2023 -0400 + + Merge remote-tracking branch 'origin/fan' into rc_1.1.1 + +commit f7889fb3c95d5ec915b929b75a332a6e539e8087 +Author: Alexander Raistrick +Date: Sat Oct 14 23:50:54 2023 -0400 + + Fix GeneratingIndividualAssets + +commit b3d9d6f14c9d617d75a397dea6c96f95934adc46 +Author: lahavlipson +Date: Sat Oct 14 22:10:52 2023 -0400 + + Optical flow / ground truth fixes and refactor + + * Refactor. + + * Scaling no longer needed for optical_flow_warp.py + + * Update compile_opengl.sh + + * Refactor optical_flow_warp.py + +commit ced0e871a740785bb0c366607b215244b836d6c0 +Merge: 9310e7b7e d2246f493 +Author: Alexander Raistrick +Date: Sat Oct 14 22:38:49 2023 -0400 + + Merge remote-tracking branch 'origin/rc_1.0.4' into rc_1.1.1 + +commit d2246f493679fa973271155179b826c5956dfec7 +Author: pvl-bot +Date: Sat Oct 14 21:28:29 2023 -0400 + + Update CHANGELOG, add copyright comments + +commit b2d4e47ecf325d5fd2346b8041b3ebc966046071 +Author: lahavlipson +Date: Sat Oct 14 21:10:14 2023 -0400 + + Infer buffer_size = 2 x image_size + +commit 90b87310787b99f4bde004e0b8cd045442a762ff +Author: lahavlipson +Date: Sat Oct 14 20:53:10 2023 -0400 + + Update flow scale. + +commit 93c0bef3280fccca042ad5cfe75301a32385abda +Author: Alexander Raistrick +Date: Sat Oct 14 18:36:08 2023 -0400 + + Create dataset download tool, pregenerated dataset docs + +commit f1a27a93b858e9dee32b389e0694b98ce50f9485 +Author: Alexander Raistrick +Date: Fri Oct 13 15:55:12 2023 -0400 + + Bugfix tag segmentation, render throughput, upload cleanup, crashing on symlinks + + * Revert monocular video fineterrain/populate ordering + + * Fix jobs getting stuck due to max_stuck_at_numdone settings + + * Fix upload cleanup + + * Fix crashing on symlinks + + Bugfix tag segmentation, render throughput, upload cleanup, crashing on symlinks + + * Revert monocular video fineterrain/populate ordering + + * Fix jobs getting stuck due to max_stuck_at_numdone settings + + * Fix upload cleanup + + * Fix crashing on symlinks + +commit b7b431f90d106a11154a7d9d27b8835505e97e2c +Author: lahavlipson +Date: Fri Oct 13 19:08:02 2023 -0400 + + optical_flow_warp.py bug fix. + +commit 75994298ccc60e24aae838ac16b51a15172bde42 +Author: Alexander Raistrick +Date: Fri Oct 13 18:05:30 2023 -0400 + + Allow RCLONE prefix via ENVVAR, allow more general prefixes + +commit f94eebda675cdc8748319fa7265e881d13c38de8 +Author: lahavlipson +Date: Thu Oct 12 20:36:38 2023 -0400 + + Refactor. + +commit 6a49d6fe586175537dc98459f0d470b82731579e +Author: Alexander Raistrick +Date: Fri Oct 13 17:14:04 2023 -0400 + + Standardize image suffix format across the whole repo + +commit 98acdb8e1e147bda9fc295460ef3db695ad1751a +Author: Lahav Lipson +Date: Thu Oct 12 15:12:05 2023 -0400 + + Ground truth scripts update oct5 (princeton-vl/infingen_internal/#86) + + * Bug fix when rendering instance ids. Remove indents in Objects.json. Save instance_ids to Objects.json. + + * Update optical_flow_warp.py + + * Update depth_to_normals.py and requirements.txt + + * Update depth_to_normals.py + + * Update rigid_warp.png + + * Update instructions. + + * Update segmentation and bounding box scripts to work w/o tags. + + * Update comments. Use get_frame_path. + +commit fdef6f268bd47324f561d23ecdd8c48e48c84f4a +Author: Alexander Raistrick +Date: Wed Oct 11 16:06:16 2023 -0400 + + Implement retar for release + +commit 3e855cba2d6d91639a459793fdc81d818d3f7e26 +Author: Alexander Raistrick +Date: Sat Oct 7 18:53:27 2023 -0400 + + Foolproof interpolation specification, handle mask and compressed npz cases + +commit b69b87d1701b95a080a3318903fc56d999cdd1fc +Author: Alexander Raistrick +Date: Wed Oct 4 15:33:14 2023 -0400 + + Implement frames folder reformat, update torch dataset, make dataset mostly standalone + +commit a188fe18dfb21e75e25be837e0e9c612cc180e29 +Author: Alexander Raistrick +Date: Sun Sep 17 13:41:04 2023 -0400 + + Data release toolkit + + * Implement format fixer + * torch dataset example + + * Tar-by-tar version of data toolkit, + * resize groundtruth, + * optimize jsons, + * fill missing poses with dummy dicts + +commit 9310e7b7e440bdc84b5671a65baceb0f7db068d9 +Author: Alexander Raistrick +Date: Wed Oct 11 14:28:00 2023 -0400 + + Update landlab and numpy, fix bnurbs import when using minimal install + +commit 0306641b6899a825f698a29649bc9aa874c7e937 +Author: Zeyu Ma +Date: Tue Oct 10 03:23:08 2023 -0400 + + terrain tests + +commit 67d9b17ae9b72024a684e4e84210474c5a5cad39 +Author: Alexander Raistrick +Date: Mon Oct 9 17:39:07 2023 -0400 + + Add INFINIGEN_MINIMAL_INSTALL flag to disable ALL compiling when doing interactive blender install + +commit 464a3f7a66c42f123481c9b6c9d53c679f9f994c +Author: Alexander Raistrick +Date: Fri Sep 1 15:22:53 2023 -0400 + + Update docs, fix runtime issues in pip installation + + * Update docs, add json to packaging, fix .soil file loading + + * Update docs to remove all references to old installation strategy + + * Move tools back to infinigen/tools, update all python invocations to be -m calls for compatibility with pip-only installs + + * Dont require wandb, dont even import it unless explicitly enabled + + * Fix erroneous gin logging, disable non-warning logging for all child packages, make all code use a non-root logger + + * Make fluid installation a fully separate post-install step, and dont attempt to init the module unless it will be used + + * Import ordering fix + + * Add cibuildwheel config, fix python -m build crash + + * Fix infinite runtime on hello world blendergt + + * Install script for interactive blender install + + * Move color util to fix circular import + + Fix rebase typos, fix torch_dataset imports, fix interactive_blender install + +commit fd3592344ef4f0f9b8d3cdd4feb10d2588190263 +Author: David Yan +Date: Wed Aug 30 23:13:33 2023 -0400 + + ast rendering fix + +commit 593876afb822da756b42fd56618233e08826dcd8 +Author: David Yan +Date: Wed Aug 30 22:21:29 2023 -0400 + + bpy module multiprocessing fix + +commit 6b59ddae188de20cff361802489f24eb9a460ae2 +Author: DavidYan-1 +Date: Mon Aug 28 15:44:03 2023 -0400 + + Update docker for pip-installed bpy + + * docker fixes + + * wsl docker fix + + * remove opengl compilation from docker-run + + * docker editable install + + * dockerfile type + +commit cba91ca596c46cd8f78a693b1b2650ff2b35cf1e +Author: Alexander Raistrick +Date: Mon Aug 21 12:31:47 2023 -0400 + + Working pip installation + + * Update test import mechanism to use better specified paths + + * Move nonessential tools out of the infinigen package dir + + * Dont build terrain etc during tests + + * temp commit txt package data + + * Fix tests and runtime errors when running as a package + + * Refactor config loading, fix relative paths in config, html, json pallette + + * Move examples to infinigen_examples, test execute_tasks, make all config imports relative + + * Add manifest.in, simplify pyproject.toml + + * Convert surface registry to relative importlib style + + Remove old docs, single-source the package version + + Fix misc test warnings, disable egregiously slow single asset tests + + * Final painstaking fix for pyproject.toml to include all compiled files & data files + +commit eaaa8162a1647442e511bc2e1a35f1a2c061d8e5 +Author: DavidYan-1 +Date: Mon Aug 21 15:09:36 2023 -0400 + + Makefile switch back to rm -rf + +commit c367b6bdf5106a9eb4820123ea6d9201f8719586 +Author: pvl-bot +Date: Sat Aug 19 15:47:39 2023 -0400 + + Docker build & editor config from "Various docker fixes (#22)" + + Co-authored-by: datashaman + +commit 887cd553df405e37ed82ff19c95a207eef13f5d8 +Author: Alex Raistrick +Date: Fri Aug 18 12:24:53 2023 -0400 + + Fix everything that didnt pass tests / checks + + * Fix non-compiling code found by linting + + * Fix non fatal linting errors + +commit 0228fde529258a2b366450d75ae2db78f133ef58 +Author: Alex Raistrick +Date: Thu Aug 17 17:44:34 2023 -0400 + + Unit tests + + * Set up pytest + + * asset tests + + * material tests + + * hello_world tests + + * Iterate github workflows + + * Update tests + + * fix gh actions + + * Remove installation checks for now + + * Ignore dependencies folder + + * Test improvements + + * Fix unit test commit pyproject toml + +commit c4a14d6f89fa40a00d5e2f575946c0d851eba0c5 +Author: Alex Raistrick +Date: Mon Aug 14 15:17:47 2023 -0400 + + Create setup.py and configs, minimize dependencies + + * Disable blending.py, make pallette requirements optional + + * Move marching cubes into toplevel setup.py, remove all python command invocations from CMake + + * Add pyproject.toml, convert bnurbs cythonize to toplevel setup.py + + * Leave terrain/opengl as independent install scripts + + * Move version to a txt file + + * Install flip fluids into pip bpy's addons folder + + * Reduce commits via better __init__.pys + + * Use pip install -e . in setup.py, update infinigen_gpl pointer + + * Move remainder of install.sh to setup.py, move version to __init__.__version__, add build commands to Makefile + + * Only run build_deps in the right steps, add options to disable terrain etc + + * Move scripts to examples + + * Tweak setup.py + + * add tabulate req + +commit f1c3e24dd8dd51b21d07b8a8c806279a04fdf58a +Author: pvl-bot +Date: Sat Aug 12 17:47:33 2023 -0400 + + Fix all imports and paths + +commit 6265e9a6bef5a98c16f41be7f551f0339cfceec9 +Author: pvl-bot +Date: Sat Aug 12 15:04:22 2023 -0400 + + Reorganize the entire repo (breaks imports) + +commit 3c767644952044b96bbe4415e91d50f8ab696854 +Author: David Yan +Date: Sat Aug 12 14:40:47 2023 -0400 + + Update to 3.6, fix installation, fix all existing asset code + + * working install.sh + + * assorted 3.6 compatibility fixes + + * fixed transfer attribute and added kernerlizer nodes.mix code + + * group input value fixes + index -> name specifiy + + * rename asset_grid and fixed blender path + + * bird fixes + revert geometrynodes.py transfer_attr + + * tiger, snake, transfer_attr compatibility fixes + + * chameleon and sculpt transfer attribute updates + + * Coconut Tree Fixes + + * Tree Fix + + * Assorted Fixes + + * Fish, Bird, Flowering plant fixes + + * Dragonfly, Chameleon fixes + making more assetfactories discoverable + + * Ivy, Lichen, Treeflower Fixes + +commit 1100f52b49f474a602bfd2b14b9f4729d44691e6 +Author: Alexander Raistrick +Date: Sat Aug 12 14:38:50 2023 -0400 + + Initial buggy 3.5 fixes + + * pip-based install script + + * Update docs and manage_datagen_jobs to use conda python + + * Copy over 3.5 nodegroup interface fixes + + * Fix duplicate Value nodegroup input kwargs + + * Implement compatibility mapping functions to catch old code using no-longer-support blender nodes + + * Handle both commandline formats in argparse + + * Update transfer attributes to ignore hidden attrs + + * Fix all Msample default_value interfaces in mingzhe materials, fix remaining StoreNamed + + * patched butil and mesh tools + + * blender_internal_attr + +commit e393b667ab89de5498ff2ce374b8756a18e9ada0 +Author: lahavlipson +Date: Tue Sep 5 10:49:33 2023 -0400 + + Fix opengl not writing ground truth for second stereo image + + * GT bug fixes sep4 (princeton-vl/infinigen_internal/#81) + + * Misc bug fixes. + + * More bug fixes. + + ---------------- + + Co-authored-by: Alexander Raistrick + +commit dd9569b8329da70596cb7576301c1942e936a630 +Author: Alexander Raistrick +Date: Sun Sep 3 16:59:56 2023 -0400 + + manage_datagen_jobs refactor and new features + + * splt into many files + + * add max_queued_tasks + + * add cleanup except_logs + + * add finalize_tasks list, move upload + + * Aggressively cancel jobs when siblings crash + +commit b26f5654804870d9c22d09a237daa9a517edc498 +Author: Alexander Raistrick +Date: Sun Sep 17 13:41:04 2023 -0400 + + Data release toolkit + + * Implement format fixer + * torch dataset example + + * Tar-by-tar version of data toolkit, + * resize groundtruth, + * optimize jsons, + * fill missing poses with dummy dicts + +commit 4489715eaab60a3fbebfad512b046df3c3e7967e +Author: pvl-bot +Date: Wed Oct 4 09:50:15 2023 -0400 + + v1.0.4 - Rendering tools improvements, ground truth optimization + +commit 420664cc448ab93b8c887877a64be4581368dafd +Author: Alexander Raistrick +Date: Sat Sep 30 17:43:46 2023 -0400 + + Upload checking, fix opengl default resolution + + * Enforce that user tells upload what to do with every single file, no missing or accidentally omitted files are possible + + * Fix upload fine, disable camera placeholder to bring back forests, Refine cleanup, make camviews mandatory, opengl default to 720p + +commit 6907ac787061d3cdf5e89a2b6ce1e5b123e331ea +Author: DavidYan-1 +Date: Mon Oct 2 16:03:29 2023 -0400 + + test fixes + remove pytest from integration testing script + +commit 39bf2bffd1db2a54a0c3b62d959eba1f00bc5c38 +Author: Alexander Raistrick +Date: Sun Sep 17 23:22:00 2023 -0400 + + Manage datagen jobs refactor and latency improvements + + * Refactor and reorder upload, do metadata/thumbnail last as a sign of completion + + * Add max_stuck_at_step limiter, refactor state tracking, bring back jobs.log, cleanup/refactor some parts + + * Add command upload, add slurm_cpuheavy, tweak other configs, remove fineterrain by default + +commit 9694693de5ebca754cf8ce9a8840806b79cf8686 +Author: lahavlipson +Date: Tue Sep 5 10:49:33 2023 -0400 + + Fix opengl not writing ground truth for second stereo image + + * GT bug fixes sep4 (princeton-vl/infinigen_internal/#81) + + * Misc bug fixes. + + * More bug fixes. + + ---------------- + + Co-authored-by: Alexander Raistrick + +commit 03e27a735b1445cc16a50b55c8c3ecf32e0c4a1f +Author: Lahav Lipson +Date: Sun Sep 3 19:26:05 2023 -0400 + + Opengl updates sep3 (princeton-vl/infinigen_internal/#80) + + * Save view size and camera parameters to single npz file. + + * Ignore CURVES objects, for now. + + * Remove unused code. + +commit 3648e8571f2f762dbea5982919f6936518531154 +Author: Lahav Lipson +Date: Sun Sep 3 19:25:36 2023 -0400 + + Reduce storage costs & make segmentation masks easier/faster to use (#77) + + * Add compress_masks.py + + * Call compress_masks.py in opengl_uuid.sh + + * Teensy fix. + + * Teensy fix. + +commit 6a1e2190999f77e217feee70da03943fe5875f44 +Author: Alexander Raistrick +Date: Sun Sep 3 16:59:56 2023 -0400 + + manage_datagen_jobs refactor and new features + + * splt into many files + + * add max_queued_tasks + + * add cleanup except_logs + + * add finalize_tasks list, move upload + + * Aggressively cancel jobs when siblings crash + + * Fix queueing + + * Bugfixes + +commit 32e11751eb1cd68f1b0e3de50cd3a96c95930186 +Author: lahavlipson +Date: Fri Aug 25 18:13:56 2023 -0400 + + Fix missing range-check bug. + +commit fab93f4e819084b54b150105f1b80f369e1f7c7a +Author: Alexander Raistrick +Date: Thu Aug 24 16:20:34 2023 -0400 + + Visual & Pipeline config improvements + + * Fix upload_util + + * Tune visual configs, move ocean to experimental + + * Deduplicate slurm_account settings, allow random choice of accounts + + * Print banned nodes to verify they are working on startup + + * Add commit hash and resolve paths in run_pipeline.sh + + * Tune caustics and glowing rocks chance/strength + + * Move rain to experimental due to no motion blur + +commit 51c73075b7ead5ff2a8effeb62fdb3f494beb88f +Author: Lahav Lipson +Date: Thu Aug 24 16:17:09 2023 -0400 + + 96 bit instance ids (princeton-vl/infinigen_internal/#71) + + * Update exporting.py to save 3 32-bit ints for instances. + + * Save instance ids as HxWx3 array. + + * Update instance segmentation visualization. + +commit 774346e211935b0e84eeb1228ddf5ad979e75082 +Author: lahavlipson +Date: Sun Jul 9 00:57:58 2023 -0400 + + Only load vertex indices for current frame. + + * Save mesh bugfix - untested + + * Fix compilation error. + + * Bug fix. + +commit 0f208a8044c38a797f3383d7c7f7f7425154f25b +Author: Kaiyu Yang +Date: Mon Sep 4 08:06:38 2023 -1000 + + Update ImplementingAssets.md (#142) + +commit 6919bfbbb3342041504ff1ef986b036857e27783 +Author: pvl-bot <136786582+pvl-bot@users.noreply.github.com> +Date: Mon Aug 28 16:56:11 2023 -0400 + + Hotfix highpoly terrain mesh not shown in fine.blend (#139) + + * optimize_terrain_diskusage_flag + + * no redundant glb saving + + * Disable optimimze_terrain_diskusage unless using high_quality_terrain + + --------- + + Co-authored-by: Zeyu Ma + +commit 3664df8bf5b7ff45f3911be29bd56358689fd44e +Author: Lingjie Mei +Date: Fri Aug 25 14:16:19 2023 -0400 + + Make deformed trees work again. + +commit 0ab7cd7d2507115f3228aafc06126a4d4332a9e7 +Author: Lingjie Mei +Date: Thu Aug 24 22:01:15 2023 -0400 + + Cherry-pick from e2a7 + +commit b166f66f32d789481daff0d4a01e2fd2b9e57906 +Author: Lingjie Mei +Date: Thu Aug 24 21:01:28 2023 -0400 + + Cherry-pick from e2a7 + +commit d38346baeff7b30140c1b253ca4349125837f74d +Author: Lingjie Mei +Date: Thu Aug 24 21:01:10 2023 -0400 + + Cherry-pick from 986d3 + +commit 7e2975b239d6da2ad6af05457df19db06bdf755a +Author: Lingjie Mei +Date: Thu Aug 24 20:58:55 2023 -0400 + + Cherry pick from b63c9b + +commit e5d15b76c7a46a17f03bd2809d073f0e8ea03eca +Author: Lingjie Mei +Date: Thu Aug 24 20:53:29 2023 -0400 + + Cherry pick from 14e12f + +commit f26a82e0ae5fc0b08a7d2d0cff6b9830e41d2d8f +Author: pvl-bot +Date: Wed Aug 16 11:32:02 2023 -0400 + + Hotfix flip_fluid loading, update github templates + +commit 4ae5d20c410da9ced0424e3ec69e5a5701f30ae4 +Author: pvl-bot +Date: Tue Aug 15 18:41:53 2023 -0400 + + Hotfix opencv version #130 + +commit a1edc13ce3639384cf7afd9d45b7ec6060cfee2c +Merge: b387b5a60 366836bca +Author: pvl-bot <136786582+pvl-bot@users.noreply.github.com> +Date: Tue Aug 15 17:05:46 2023 -0400 + + Merge pull request #132 from princeton-vl/develop + + v1.0.3 - Fluid code release, implementing assets documentation, render tool improvements, integration tests + +commit 366836bca07917cc312b9a7c3b3b4e5818a1ef8c +Author: pvl-bot +Date: Mon Aug 14 13:34:26 2023 -0400 + + v1.0.3 - Fluid code release, implementing assets documentation, render tool improvements, integration tests + +commit a124321d82f0e9917baa35c9a291d4dd37c6dae6 +Author: Karhan Kaan Kayan +Date: Tue Aug 8 12:19:47 2023 -0400 + + Fluid documentation (princeton-vl/infinigen_internal/#61) + + * Add hello world scene + + * Config tweaks + + * fix the camera bug and water not simulating + + * res fix + + --------- + + Co-authored-by: pvl-bot + +commit c534cd87da56afe1a00d07a30595c17186082b95 +Author: Alex Raistrick +Date: Tue Aug 8 11:13:28 2023 -0400 + + Fluid Refactor (princeton-vl/infinigen_internal/#60) + + * Acknowledge FLIP-Fluids + + * Deduplicate configs + + * Remove fluid-specific logic from camera funcs, move to scene_type_fluidsim + + * Move river invocation from core.py to compose_scene run_stage calls + + * Remove FLIP caustics from release + + * Move fire scenecomp and Cached class wrappers under fluid/ + + * Change on_the_fly to use run_stage + + * Move installation to tools/install, make FLIP installation optional and update GeneratingFluidSimulations.md + + * Deduplicate river configs, add example commands + + * Only unhide assets needed for fire sim, and unhide once done + + * Move enable parent cols to butil contextmanager + + * Remove unnecessary --blender_path + + * Typofixes + + * Fix accidentally deleted config fields in new river configs + + * Fix finding placeholders for river calls + + * Fix impl typo + + * Cleanup camera selection varname, unused kwargs + + * Catch modulenotfound errors + +commit 18152ea60abab61fa226a67180be5249c9469922 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/tools/submit_asset_cache.py + +commit f295b204bd0124ddfc11355029ab55e98910f28d +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/tools/scripts/render_video_fire.sh + +commit d870745a74329f443aa66cf7c0942e41ea17f697 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/tools/scripts/render_river_video.sh + +commit 3f5bc04819280bdf9c8580f4a57057d4736b7021 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/tools/pipeline_configs/slurm_fluid.gin + +commit 70bd6f97dde1ee0b15c14d4453bd92e62f53d6a5 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/tools/pipeline_configs/monocular_video_river.gin + +commit 419baf3623de30d1220944d04f0ff7751eddb6ea +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/waterfall_material.py + +commit f3634a288ba7559554194a238ea1ff18aa5bb895 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/smoke_material.py + +commit bf3d97e03ed3aaa1966584d15d477afc92153009 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/river_water.py + +commit 77c36a5e93a4e79eefb7b7bf9327795a10759aa3 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/new_whitewater.py + +commit d684e5693a72f34a174c2362c1253f2addd8725d +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/blackbody_shader.py + +commit 8108cd9d7fcab8c6830379f64098cb99dc03eaf7 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/unit_tests.py + +commit 4ce86c1592b922db31da3637df491112ad84f3a6 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/run_tests.py + +commit 701defdda8ee1905d105e92b5f1ee8a6af109e5a +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/run_asset_cache.py + +commit f5c2a739afb2337e02d35cd7cf3e37c266e6db3f +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/liquid_particle_material.py + +commit 9107c1873b41f27df0f424fe8a8b5c5c44c5671c +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/generate.py + +commit a129c88170d062c639192395fb2d36386419c15f +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/fluid.py + +commit af0a1d6930b05afb0d65a89f4680a95685e248ad +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/flip_init.py + +commit d2b67ae694a7a3c7b8b878f8e810a51103100848 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/flip_fluid.py + +commit a33529ee7f2247d0efa1495e69f5f15b06c3dbfb +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/duplication_geomod.py + +commit 993087f4684e8f7e72fd9c5cc85a5806d2f79132 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/bounding_box.py + +commit 26022c1d6726bf2ef09ae4f1a75ed305e0d405d9 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/asset_cache.py + +commit 9fac0fbcae1cd1483cf6cf735bea38bf51509b6b +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/fluid/__init__.py + +commit 612527eb14b91f363d28841f362faa26616fd3f0 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/config/use_on_the_fly_fire.gin + +commit 85d1f789a17b9dd1b3ab834ce4c716bdd04af595 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/config/use_cached_fire.gin + +commit 891e0f57204a42cea612e04fd7487653e6fdfbcb +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:09 2023 -0400 + + Add fluid code-release changes in worldgen/config/trailer_river.gin + +commit c17a58aedd17586158a524f5388d4230a923b5b7 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/config/scene_types/tilted_river.gin + +commit e80503a64566d8b888aab242ee45928a9910d233 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/config/scene_types/simulated_river.gin + +commit 5ee57e4905b76fa1f639cb49a3a9b24f16b81438 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/util/blender.py + +commit 6487c2a2742abfdb877aa832f855245ffb6f7e04 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/tools/util/cleanup.py + +commit ca83ec8c4ca3174dc8150909b20150ed2b61a6d4 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/tools/asset_grid.py + +commit 7115b597e4ce1f92348a32795660bdc5cec9350a +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/terrain/core.py + +commit eddd10571f893099777f3cfdcfff1b6a58843312 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/spider_plant.py + +commit 2b9481d5fdcb23d860ea2996c2339aea8be50cc5 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/surfaces/templates/snake_plant.py + +commit a98fdd9bf52fc14eb2c51540906a2c1540aabb84 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/rendering/render.py + +commit 7b7c845a152262d59dca63d2337227c34c95c963 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/placement/placement.py + +commit e687f0b8edce3007acfc956d0320a4ff483b2fe8 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/placement/camera.py + +commit 3e685b6cf20f55d57e858b5c011faccbe6d67c9f +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/placement/animation_policy.py + +commit 3388c5d532504b663bcd54fdf076875088b5c907 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/generate.py + +commit 4903651982349fdfe841c41d16f15472386553a1 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/core.py + +commit f94a2d90349131fa858bce93c72655fdbedc0d18 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/config/scene_types/desert.gin + +commit 8141784a3e977820e0dde22b108d50aefc8017df +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/config/scene_types/arctic.gin + +commit 521da2ed314d95631b5e2d4f1f902600ce85e59d +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/config/base.gin + +commit bcddd46c8330ea4fdda80b563e55796959624144 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/trees/generate.py + +commit d8a17ef130abd790f0bca36334db93fe88d9c256 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/trees/__init__.py + +commit b3133b4504a844d4fad43ac754057cac789e206b +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/small_plants/spider_plant.py + +commit 27c70b91874b74bc66123c7bdad4772f80b35c5f +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/small_plants/snake_plant.py + +commit 630a951ef43f1fa34e5979e2d46fc87158921d33 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/creatures/genomes/carnivore.py + +commit b47c24f778eeabb7c1c3d80e92acd1bfda916a3f +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/creatures/__init__.py + +commit f167d5a594b1d855d82641e5a01af965f378ae73 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/cactus/generate.py + +commit 2cf1a545fa717826e08b37d0aa9d0b9e0f8077b1 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/cactus/__init__.py + +commit fc1d9b2c5433f4bc7334e606648a25ad0ca7e4d0 +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in worldgen/assets/boulder.py + +commit a2c9960d0d33660892616c27644cac0be19961da +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in process_mesh/blender_object.hpp + +commit 91b0edaa6e93dd54cf2aa968d0d839dbd69c13bb +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in process_mesh/blender_object.cpp + +commit bfa2ab7f82b3a08b128a07f164b02b26b03f127d +Author: Karhan Kaan Kaan +Date: Mon Aug 7 10:54:08 2023 -0400 + + Add fluid code-release changes in install.sh + +commit bd69644a3b322f3af941a0aeed74022f14f33340 +Author: Alex Raistrick +Date: Tue Aug 8 17:30:28 2023 -0400 + + Implementing Assets Documentation (princeton-vl/infinigen_internal/#64) + + * Documentation draft + + * Make terrain optional, remove terrain dependence from camera logic + + * Fix config typos + + * Improve intro and blender UI setup + + * Added transpiler tutorial + + * Finish minimal implementing assets docs + + * Downsize images, add new doc to README + + * Fix image hyperlinks + + * Add testing script addition + + * Fix non-terrain version + +commit 6e17b5c798cc2f71cc35b53e6f48a9ba439038e6 +Author: Alex Raistrick +Date: Tue Aug 1 17:53:31 2023 -0400 + + Rendering improvements (princeton-vl/infinigen_internal/#39) + + * Tweak render_video_final + + * Remove random config choosing from core.py + + * Create tools/pipeline_configs/base.gin, move scenetype distribution configs into it + + * Create noshortrender config to test on IONIC + + * Implement slurm niceness override, add it to render_video_final.sh + + * Only include camera 0 in parse_video output + + * Read slurm partition from ENVVAR by default + + * Fix config postprocess + + * Fix slurm envvar + + * Typo fixes + + * Use roundrobin by default + + * Rendering tweaks + + * Change trailer.gin to video.gin with 720p res + + * Fix niceness + + * Set exclude_nodes list via envvar, move niceness configs into slurm.gin + + * Create render_video_720p.sh, start off experimental.gin but more needs adding + + * Add dryrun options + + * Fix --override vs --overrides + + * Move legacy task.Fine + + * Retool upload func + + * Add slurm_1h and stereo config + + * Rendering & typo fixes + + * Update render script and slurm.gin mem amounts + + * Fix excluded gpus + + * Add queues stats to wandb, add pandas to requirements.txt + + * Fix num_concurrent reset 24h later + + * Dont keep working on scenes which have had a fatal crash + + * Add new timeout message to error parsing + + * Fix overly nested upload dirs + + * Add thread limit to local jobs + +commit 887eb82f099ba48a59f894b8eaa50ce52fbbb7da +Author: pvl-bot +Date: Sat Aug 5 15:18:52 2023 -0400 + + Docker overhaul (#65) + + Co-authored-by: datashaman + Co-authored-by: David Yan + +commit a27e62059b2fa17f88f7a073dae5218f98d79143 +Author: pvl-bot +Date: Sat Aug 5 15:11:54 2023 -0400 + + Remove docker code attributed to pvl-bot, to be recommitted with github + co-authorship + +commit 62350548c029b2aa8cfe9a67fd9a8da29c8799ed +Author: David Yan +Date: Tue Aug 1 17:55:53 2023 -0400 + + Profiling & Testing (princeton-vl/infinigen_internal/#28) + + * basic framework + + * step times + + * multiple file fix + + * multiple file fix + + * more detailed logs + + * asset stat reporting + + * memory stats + + * fixed brightness test + + * improved formatting + + * noise estimation using PSNR + + * grayscale working + + * single image noise estimation + + * asset step memory accuracy + + * switched to opengl_gt + + * fixed inaccurate mem diffs + + * output formatting + + * increased sampling and changed noise estimation method + + * blender opengl gt combined config + + * fixed opengl+blender config + + * fixed timedeltas > 1 day breaking + + * further ground truth testing + + * aggregate tag segmentation stats + + * obj segmentation stats + + * cleaned up gt comparison (not working still) and reordered config + + * named tables + + * more table titles + readability + + * removed old copyright + + * deleted extraneous file + + * copyright + + * made internal representation of stage times more flexible + + * Delete logs.log + + * save data csv + + * object and instance counting + + * general testing usability improvements + +commit b387b5a6002b2fd2898e78a664b5754ca01e7081 +Merge: 6d0d34a11 a3a5b57ff +Author: pvl-bot <136786582+pvl-bot@users.noreply.github.com> +Date: Fri Jul 28 18:22:29 2023 -0400 + + Merge pull request #115 from princeton-vl/develop + + v1.0.2 - New documentation, plant improvements, disk and reproducibility improvements + +commit a3a5b57ffee494121a50514a8e331d4c2a42dbd5 +Author: araistrick +Date: Fri Jul 21 01:14:26 2023 -0400 + + v1.0.2 - New documentation, plant improvements, disk and reproducibility improvements + +commit 587fe9828306a8ea1ae1eacdf786700a4c87f1a3 +Author: Alex Raistrick +Date: Fri Jul 21 03:01:02 2023 -0400 + + Refactored & Expanded Documentation (#22) + + * Move existing docs + Installation and HelloWorld into docs/ folder + + * Search entire config/ and pipeline_config/ dirs, allow specifying with .gin prefix + + * Organize worldgen/tools/pipeline_configs + + * Organize worldgen/configs + + * Initial commandline documentation + + * generate.py config tweaks + + * Add help strings to all manage_datagen_jobs args + + * CommandlineOptions documentation + + * Reorganize + + * Fix typos + +commit ee36d886b346b5f26ee265051ba743f16f481766 +Author: Alex Raistrick +Date: Wed Jul 19 19:17:07 2023 -0400 + + Copy in terrain onthefly diskusage improvements, update infinigen_gpl to add .gitignore + + Co-authored-by: Zeyu Ma + +commit 0ff02d6fc3d65048e5ce4e320cdcc0200d6efc01 +Author: Zeyu Ma <31351547+mazeyu@users.noreply.github.com> +Date: Thu Jul 20 07:16:47 2023 +0800 + + Reproducibility of asset placement + + * mesh_to_sdf as included code + + * replace mix with explicit formula + + * use relative path of mesh_to_sdf + + * comment out opengl + + * add pyrender req which is prereq of mesh_to_sdf + + * uncommenting back + + * use float in mix + + * trimesh force version + + * face ordering + + Add MIT license for mesh_to_sdf + +commit 18685454f5c411cd2eda6247e1a35a08e4afe75c +Author: Beining Han +Date: Wed Jul 19 13:35:17 2023 -0400 + + Add spider plant and snake plant + +commit d42a77b4126696ec8d3dda004057039892cc45ba +Author: zuoym15 +Date: Wed Jul 19 13:18:45 2023 -0400 + + move over tree branching code + + * move over tree branching code + + * performance bug fix + +commit ccee42c14a7bb3b084019565c92c00639d8c1530 +Author: araistrick +Date: Thu Jul 13 12:04:59 2023 -0400 + + Typo-fixes by tms-gvd (princeton-vl/infinigen#76) + + Co-authored-by: tms-gvd + +commit dbb6d1c63a37712a71a606ddd18609546a8790e7 +Author: araistrick +Date: Mon Jul 10 04:24:41 2023 -0400 + + Refactor rigging, fix IKs, smooth before remesh, fix running, add end trim (Fixes princeton-vl/infinigen#89) + + Copy in import blend devscript + +commit a28da5ad71eda6d616e47c27ed3242b49750ed7c +Author: araistrick +Date: Sun Jul 9 02:10:11 2023 -0400 + + Hide culled placeholders to minimize effects of white cube bug (princeton-vl/infinigen#86) + +commit 209c803f752484fc8a2310645e2448364371f0cf +Author: araistrick +Date: Sun Jul 9 01:55:50 2023 -0400 + + Clarify crashed.txt (princeton-vl/infinigen#95) + +commit 6d0d34a115ed5f9e453fa010c1f4a9038e9dc5c3 +Author: lahavlipson +Date: Mon Jul 3 15:43:58 2023 -0400 + + Revert "Update requirements.txt" + + This reverts commit 941880a4a383ffb15bdc7b288fb1fa3b5f7f6ec1. + +commit aa22d4d1d4cb30de12f6aac453e6f49b87b787db +Author: Lahav Lipson +Date: Mon Jul 3 01:22:06 2023 -0400 + + Gt utilities (#83) + + * Update GT utilities. + + * Overhaul GT visualization for blender's built-in GT + + * Refactor. + + * Flip camera pose axes to be consistent with computer vision. + + * Save camera parameters during render step, not during GT. + + * Fix bug with blender's built-in segmentation masks. + + * Flatten object data json. Remove redundant information. + + * Make built-in GT metadata and OpenGL metadata consistent. + + * Misc. + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update README.md + + * Update requirements.txt + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + + * Update GroundTruthAnnotations.md + +commit 97b8a415f8b4733c4fc0ec6bdd93dfd4aa4c4a44 +Author: Lahav Lipson +Date: Sat Jul 1 00:37:36 2023 -0400 + + Remove old files, update file headers. (#82) + +commit e0b75a8b4359003ee98bdf742c03247e158c0024 +Author: pvl-bot +Date: Fri Jun 30 03:53:40 2023 -0400 + + Infinigen v1.0.1 - BSD-3 license, expanded ground-truth docs, show line-credits, miscellaneous fixes + +commit 3040c22ba751ac28d8acca3972d846f269971ffe +Author: Zeyu Ma +Date: Wed Jun 28 22:39:39 2023 -0400 + + Code separation + +commit 1ebed6765fd185c7757766bdd40a856f3f57fbe5 +Author: pvl-bot +Date: Wed Jun 28 18:26:21 2023 -0700 + + Add acknowledgements + +commit aeb7fd0556b7aa2c34e17d53a9b26741156904a2 +Author: pvl-bot +Date: Sun Jun 18 00:46:48 2023 -0400 + + Switch to BSD 3-Clause License + +commit e52bf9f0045dae127d403132a8ea8a2900aa2a9a +Author: Soney Mathew +Date: Thu Jun 22 05:54:30 2023 +1000 + + Update README.md (#2) + + Blender documentation says it's `-noaudio` instead of `--noaudio` (Tested in MacOS Ventura 13.3.1) + +commit e059b8cf310c5d94c34deb0679f13f9da3291606 +Author: Jordan Hubbard +Date: Tue Jun 20 21:24:59 2023 -0700 + + Change submodule paths. + +commit 313a05d780d4dd4f358ec1a4c462caa8268e2594 +Author: Pvl-bot +Date: Fri Jun 30 03:11:45 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/land_process/erosion.py, fix SurfaceTypes as final commit + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e08a6184b09a17d326cc64d51259173e527598a2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:45 2023 -0400 + + Add 66 lines last edited by Zeyu Ma in worldgen/terrain/land_process/erosion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56329e9f3a058bafd27c136cf596fefa1850e0f7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:45 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/land_process/snowfall.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 83151be13cca1c77428c9adcf7616cc4079a02b8 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:45 2023 -0400 + + Add 73 lines last edited by Zeyu Ma in worldgen/terrain/land_process/snowfall.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4cc24cce7554ea3ad4212cc06197c2c7e13f4d22 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 13 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/meshing/cube_spherical_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2a21f2d3569beeb00e3de6d75f6ec2071951621b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 1240 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/meshing/cube_spherical_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0ddfffb512c7675c64bdc679678782a35dcb95a4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 14 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/meshing/frontview_spherical_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4707b1c20d8e7fb574f2404b7e9034ed5c622ba9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 958 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/meshing/frontview_spherical_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d1c09900b0dadada0328749b12b6143ae765c18b +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 14 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/meshing/visibility_test.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9abbce21271db762a66be81a22bdb14f7c081c46 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 729 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/meshing/visibility_test.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9a8ec0fb964c3a6544e94c7d3ce71090067ed1d7 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 7 lines last edited by Lahav Lipson in worldgen/terrain/source/cpu/meshing/utils.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b47a3811069f8abea32db6cae584856b8f5d7ba7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 13 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/meshing/utils.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c21bd16dac9cf7b67ddfde98a0a162b4a3895451 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 274 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/meshing/utils.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cea5de37431e445f4f2fdc2e95e75784cba7317a +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 12 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/meshing/uniform_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b38a82b4ccf4fdf45bac85db661515bda519c7f1 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 310 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/meshing/uniform_mesher.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40c8dea09dc35dbb4de73958ffc561bb1bb649ec +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/mountains.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56a12c342b8523f6a86b372ea1109523a1805736 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 16 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/mountains.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 06cbc998082ca92b962eca2b5a85e338c047cf50 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/ground.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 25cddd8a2b273f470ab8de2e721280f8a6b3147d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 21 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/ground.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb401d0e9d1f1438dab5aa305a9e007c92cb94e2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/warped_rocks.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f27d0dca660d3516f9d2e58ab85946b37a218863 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 21 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/warped_rocks.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dc0c3adf1e0a4ed5d16d15bf6f9f5168c47174d2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/atmosphere.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 73c750c1816ba0524d01b7dcb63be6c8497dcebf +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 17 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/atmosphere.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6821006fc83806e39251694bad9c6c19d0eb37cf +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/waterbody.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3be2bd470c4f47e20ec3d96645102006eb67e42a +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 19 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/waterbody.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 946c4a9e411297a0ce1471aeab017e544405f3d3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/voronoi_rocks.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7dffb8b4688ef14d0107fd5b84bf5e0355362fa3 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 21 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/voronoi_rocks.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 188697887a228ea66d5750114a9cb827f75e5339 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/source/cpu/elements/landtiles.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db368388b5d16488045028f1a6e082cadae40103 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/landtiles.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4cba457063c3cd8bbbd2daa5c2e8c982c0a982f1 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 20 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/landtiles.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 76e261bdb94169aac159a6f4e0a5b312ccdaf9f8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7fa201a6c9fe53d34dcc2d8cc6903466e73be6f +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 22 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a9089cc6278d5a6e9c33bea66c1935d9b02719a0 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in worldgen/terrain/source/cpu/elements/upsidedown_mountains.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6de8d76e7b953c3bd61fe9c2daa71ef6516de826 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/upsidedown_mountains.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 501fd9f085f5a5ddee03a6601dd7427f1aafc9ca +Author: Zeyu Ma +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 12 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/upsidedown_mountains.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f9bd2e742b066121ba679c79321724f2b0e4d368 +Author: Pvl-bot +Date: Fri Jun 30 03:11:44 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/elements/core.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 003aee9267b4655462ee861e656fc545d3eb6969 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 41 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/elements/core.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5199a2384c3c0d9337ca247896b93098531dd9b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 28 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/ice.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 369fb806f6f2924864a11621081bcf26f3b19449 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/cobble_stone.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 937752205c60d041230c2abb468ce0c6de1fe1ff +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 30 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/stone.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 50a4ed6ecaaed4a61c22ed98740db8b9ea45c8e8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/sandstone.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 436b443751fce727a585d01209e1257fd61953fa +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/sand.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 33ff252a4fe5196f03bc6bdf317f401ce791eb81 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 27 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40365f0c6490369e7ea59f66ebdfd744fef47013 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/dirt.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7b1da1c7f848e7ec15a78d82868102624a6f44b1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/soil.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ec0e1b753212309f8a824c4640e8d87c58dc5cb0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 28 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/mud.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dc6d2011cc54d37ad55ee2b194165ca72060d0e7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 27 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/snow.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 53bc800b7f3654f1c510e10cf5f4b07c12b85ffa +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 30 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/chunkyrock.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1e842865fa7dddc06e2aacba8f00fe78d70b9f87 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 29 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/mountain.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d759200f27e0b8072fcc4bb93ea32c6300a46e4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 28 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/surfaces/cracked_ground.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 86237c404769cbaff4c36e96f9fd291341348082 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 161 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/soil/rockgravelpebblessand.soil + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 05fb751ddb5dc49c61c836168255a58f9baa4932 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 63 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/soil/default.soil + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9f395cd3332cb94a40d489e9bdf3fb868fd60e91 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 60 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/soil/sand.soil + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c4808672fcf1c6a699d99512456a5f48f2f455b +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 118 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/soil/rockgravelpebbles.soil + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 44cd7b9090c6d05ceade24602bb01b5b527d35da +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 108 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/soil/rocksand.soil + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 245370e0bd9546556d3665f91d09f5b4bf405422 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 114 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/particle/particle.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f90c35d48af8c56ff3470dbc8bc705ee403e7de7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 156 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/particle/wind.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1eef059900f6be5f89847d4af43478b5f7e6aaf6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 385 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/particle/water.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8faa28325cdaec81c0eb6b4e9b2b5eb0d43be3ff +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 84 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/include/distribution.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6cc1edd468079585ab0de8f1bc4d6a44696686b9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 2586 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/include/FastNoiseLite.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 237997aa1ede3e3a95afeca06dd95ce74128ca8b +Author: Pvl-bot +Date: Fri Jun 30 03:11:43 2023 -0400 + + Add 360 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/include/vertexpool.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8c6dc05fc2e8b2e8af80f29faf08c3a13678a4c6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 247 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/io.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e093438c2b6ea9b0910510f96934918b11196c3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 122 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/surface.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 272c4958492945d6d1fe798877a905006758786e +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 627 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/layermap.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4ca6cc2e51c3a83d93e5220a0c587f14042c5473 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 79 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/soil_machine/SoilMachine.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit caab2aac7a4ffda7e8b9d223aaffc59d64931ba2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/terrain/source/cpu/utils/FastNoiseLite.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 72ee992a0b1b1d792e046f2d3a2b286dc0a9763b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 21 lines last edited by Zeyu Ma in worldgen/terrain/source/cpu/utils/FastNoiseLite.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 464ddf83726a026930eb91b8b746159d9e0832ac +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/atmosphere.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f9e5114f247364dd9b701376aec0a480128328e0 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 39 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/atmosphere.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9b5f4a57a59d4d716923958136177ed35924a962 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/ground.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3fd179b7acc87988ba75396908d3f998c993f8a9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 47 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/ground.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cbb2f994cbda949af95f03622a2506a589624fc1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/upsidedown_mountains.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1596141a911ab2bb592ce6da75168016c3afc605 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 12 lines last edited by Lahav Lipson in worldgen/terrain/source/cuda/elements/upsidedown_mountains.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d7985a4e18e6729246b25360ea432a920e80bb2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 31 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/upsidedown_mountains.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b80bc4085034c2b9a17275a443df6b5e1a97f736 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/core.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dd603c9b6e678603089fcb519af06e97b10dbc27 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 54 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/core.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c871c3cf4cf14620876b47b1c8f1177dab8c87b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/voronoi_rocks.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5f5f1f4e8c80fff433e7e3dac98a53e18b4f83fa +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 54 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/voronoi_rocks.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4e9343756cf3fe0469dd0b256a39d6901396088e +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/waterbody.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit abf703e63e345150d1176e9b152cdf3e6c6c1c0d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 50 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/waterbody.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2282d9f18673239d2440b87376b26b949ead7d0a +Author: Lahav Lipson +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/terrain/source/cuda/elements/landtiles.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1dd4d713dd7d3ce04025a337054c1b02ac0c7264 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/landtiles.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d19659d5381bb7b28b58dc5c1056c1ca694eda2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 49 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/landtiles.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4eea05fe37a778b1e8b95c09d20280fdf23a28cc +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 10 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ffd50b367e3fdc83a4006e86de4115a240ba3d7d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 22 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a0fbbf7e7d494f3db734aa5fa7a3e7a86d5a39a6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 10 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/warped_rocks.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d423c2cb8cc94e397ec7cd42440bd974f9b866f2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 50 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/warped_rocks.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c1c39bf80c8aee2ceee76085c0736ec8f80f3029 +Author: Pvl-bot +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/elements/mountains.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 729397bdc13ef52a0c79feb62084d95d48e29856 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:42 2023 -0400 + + Add 36 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/elements/mountains.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b73e2318b9b37e5a4781201b5ceaf98f29b7fdac +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 64 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/chunkyrock.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2d3f11fcbadecfc4081a2dc10154e4f3a0c6883b +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 58 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/sand.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74cc178655a18a4e8f6437e9940d5116fe232559 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 55 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/ice.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 24ddf1df072af0ca39903122fcebd09a80c9d655 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 64 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/stone.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e68462f69a2901cf3bf9b3d04da591588bb31ce3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 54 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/cracked_ground.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 08d6a43f354f2ab35a9f1a25ce15f4db82a2e082 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/sandstone.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 55c1cb90a89725bf7e2b2e3ea4dc1747ff6bb927 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/cobble_stone.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59f7c2472a3ba189d6869e748358cdc5b5106efc +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 53 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/mud.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97e6f45b47b1dbcb51f868f5a9df18dabad137d3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 38 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/header.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 45cf420485c891f85970881868cd3b1a0d2cf192 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 50 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/snow.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6dd21da6250bd04356cda8678ba4e60453650867 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/dirt.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 89e8ed2ae95cecc1687b35c1a254e91f03f5095d +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/soil.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 92aaa2da89d795f03df9f2e3a68080579784a796 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/surfaces/mountain.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 155e2ddafbf513140a30a871705c430f5d7056a3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 10 lines last edited by Pvl-bot in worldgen/terrain/source/cuda/utils/FastNoiseLite.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 926bafa3ee1e6dd3ae16d0a7fcdf1153112c47ba +Author: Zeyu Ma +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 42 lines last edited by Zeyu Ma in worldgen/terrain/source/cuda/utils/FastNoiseLite.cu + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 339047ad282ddce279e792985e5c1250802eff07 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 165 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_map_range.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f6a9c916e165655a3a156c7674280547bdd7c283 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 89 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_tex_noise.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 502cb910f2363a32e8c13dd7be78a23cae913c7a +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 92 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_tex_wave.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit baa93b3ae059a66222f637731d10ded2f0743100 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 290 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_texture_valToRgb.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d6ebfabfd22022865d3b5042d86b72c083eb7e5f +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 32 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_sepcomb_xyz.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d2b4d841b1418245b761103c287d3b3096ee4526 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 190 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_tex_musgrave.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 15dab862fe5facaa9b5a5d8d888c21d1c36bf032 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 58 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_vector_math.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 68a265c6d8b721e388d4300d92419eff6afec1f7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 278 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_texture_math.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b6d47fb015d92f660e64b0d5ddc66ca56c60995 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 32 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_mix_rgb.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff6a1cd4afbed04abf01240156bc1da5ae616547 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 437 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_shader_tex_voronoi.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b42021ef16240da77c54e34f41e22b3b4717ffaf +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 59 lines last edited by Pvl-bot in worldgen/terrain/source/common/nodes/node_float_curve.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 236b7cbee28dd8aca08a9730d61a6f8f669e5821 +Author: Pvl-bot +Date: Fri Jun 30 03:11:41 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/atmosphere.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f5f509d0611eff0e89a89c3adf27abb308aba4f2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 33 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/atmosphere.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b7690e68d124e8d37bdbeca17adb7d2144c13548 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/ground.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 633a44b5e18ed10e390113e520fff7869d72158c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 65 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/ground.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 64bd3ad4b468ecf2c2749f68544be6bc516286f7 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/terrain/source/common/elements/landtiles.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a3be16231109394430bd0231ec776a0234f235ff +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/landtiles.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cbfcaaf8f8e77ffec114e00d1e1dfa36f02de1c6 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 187 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/landtiles.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3b9cd2f52a9aa0c33dc3661d87ee3b3b32f25d74 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/mountains.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit da1dbba68d740e396a00c088abbcecff63481055 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 55 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/mountains.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bc2fe2b781e56ba3f1a1f91f52b03863e74e0e10 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/waterbody.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd49f7079ac1b5d519053f0bd8d9e983a9acfb88 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 35 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/waterbody.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cf9a114ff3c2aa5f8e34df237a13746eb4ad0ce3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/voronoi_rocks.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 39f69a73ffb39782d30b1615353dad574dcddddc +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 154 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/voronoi_rocks.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 35aaba8cdb981101fc06384827833b024d0550c4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/caves.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 04fe96eef38a20836cc19822d66bb3244357bc09 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 96 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/caves.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 31c91c81d3ddfcc597342b93af62d346f045337d +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/upsidedown_mountains.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 175cf0f103c4a03aa79bea09d6853f2d4d2022e3 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 7 lines last edited by Lahav Lipson in worldgen/terrain/source/common/elements/upsidedown_mountains.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5fd91e8ae624bdcfdc1c76192bb15ce8646e2ed8 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 63 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/upsidedown_mountains.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e5f708c702b4021080356b9883712cd3952dd2e9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/elements/warped_rocks.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d7d9c94b8cbf00ca80b277b93d0788f1074a04a +Author: Zeyu Ma +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 56 lines last edited by Zeyu Ma in worldgen/terrain/source/common/elements/warped_rocks.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cffd09694da75c829ab67e4b76fdf9f02f48d1d5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 189 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/chunkyrock.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7591e81f00e227a0517fadddd4ebc2e411072447 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 63 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/ice.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2cb748c507f8f47140facac762903aae6a3c2b6f +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 614 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/sandstone.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5949feaa2a32b25bfa4b19a452638fa2e5d20376 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 339 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/mud.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2d4631b73d45e8b23b678e60b4c7a88ad82f34d3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 185 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/cracked_ground.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2020f0c2600c34cdda00c5ebdc8c460885b1aa50 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 1296 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/mountain.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4cfafaa3970a8cb1c1639a65bfd844aab9b6c8b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:40 2023 -0400 + + Add 90 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/snow.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7f88ca8668a49965c68ff53b55593e6cf0f830b +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 376 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/stone.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fcaf59c7c1cdd34ac19c106729125b87e5b68150 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 399 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/sand.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1fd009838a0ac43954e94da7b12afbc6cc3fd380 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 306 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/dirt.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d71858dc7d5fe4d971edb29a73e2f8bd522ca6ab +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 309 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/soil.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5dabf8f9e52152a444edb1166aba83cdef87f023 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 152 lines last edited by Pvl-bot in worldgen/terrain/source/common/surfaces/cobble_stone.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b01daf75db77e3e16d105469d69ff5e24e886405 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 829 lines last edited by Zeyu Ma in worldgen/terrain/source/common/utils/nodes_util.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a77fbafb1f8260d604b3f3b8b113bbab28365074 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 585 lines last edited by Pvl-bot in worldgen/terrain/source/common/utils/FastNoiseLite.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 33e8e7a621d5b0a315a4cae76f83f8bc88c30cbe +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/utils/vectors.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 52af8720e8cfa833a480fda090989b73e8ab6dbd +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 189 lines last edited by Zeyu Ma in worldgen/terrain/source/common/utils/vectors.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1946c2afd30ce28a7703579f7cb234d057645388 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 2498 lines last edited by Pvl-bot in worldgen/terrain/source/common/utils/blender_noise.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56a7e030f0118ac0b2490ffac7ddbd5e67e3b9d8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/terrain/source/common/utils/smooth_bool_ops.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 085e45db314f87c278ce244497cd50613c8be70d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 9 lines last edited by Zeyu Ma in worldgen/terrain/source/common/utils/smooth_bool_ops.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 463d6d70b3f6d99b91b7eb2f677f5f80b74dcbd2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/source/common/utils/elements_util.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed0edd28e8795083e41f85d2a1d8ab8236c4f31f +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 167 lines last edited by Zeyu Ma in worldgen/terrain/source/common/utils/elements_util.h + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5503a285c437cf819f7d9f5b406a4e02c0cd0ab0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 2 lines last edited by Pvl-bot in worldgen/terrain/assets/caves/cfg.txt + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ad26966f4914b10ba53342462713bf6d6e0ef5e8 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 32 lines last edited by Lahav Lipson in worldgen/terrain/assets/caves/cfg.txt + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5909a7a6c0820410ab4a347cf81d495f5f40e668 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/caves/geometry_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af6ca6179b9ba38eff220c0ab126b7bb8d64858d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 30 lines last edited by Zeyu Ma in worldgen/terrain/assets/caves/geometry_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ceb8aae3edf812fcb3dcf1072e4d55af448fcc5b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/terrain/assets/caves/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a7d7141a83eee19d72427716759d2bf31a4ba645 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/caves/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7c6fa86c44b8b35b92806a8a6d52a7c4a1babf43 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/terrain/assets/caves/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e03401b786fd8a8a6db1808a7b7ac40af8149506 +Author: Pvl-bot +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/caves/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a70048d6e89af95df577a4adb6d6bfd1cf079b06 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:39 2023 -0400 + + Add 104 lines last edited by Lahav Lipson in worldgen/terrain/assets/caves/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d5f0d1b88e6da0beb8053124b22e4c8b06e17096 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 113 lines last edited by Zeyu Ma in worldgen/terrain/assets/caves/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd7ff5a567af667c8b7c3e0e4c4a25165f511c24 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/caves/pcfg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 066cdfba8d01c0bcf8ff16e7ad126543aab5f135 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 11 lines last edited by Zeyu Ma in worldgen/terrain/assets/caves/pcfg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 98525d810439e8a063ed25b89bbcd8591c42d9a8 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 47 lines last edited by Lahav Lipson in worldgen/terrain/assets/caves/pcfg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 397bd5530a4fb4be94bf9125f7cd7c90537d7a03 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/landtiles/custom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c220ce6da281667233787857566bd74b48b09d3c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 195 lines last edited by Zeyu Ma in worldgen/terrain/assets/landtiles/custom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c46f9f677d742ed199564662780ffc9913406fff +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/terrain/assets/landtiles/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fe998030be082ae2df5632563f1ca116d0971083 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/landtiles/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit da9784861325ab948f687b9ec73eb82711259341 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/landtiles/ant_landscape.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e2de8cfe4274d72f5aed3aefb359df49716efdcf +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 64 lines last edited by Zeyu Ma in worldgen/terrain/assets/landtiles/ant_landscape.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ef4abe6c355c9f61df886348732814112f6d5fdb +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/landtiles/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 32dc7898ae60f0fab244dcac39b6065c034a654a +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 137 lines last edited by Zeyu Ma in worldgen/terrain/assets/landtiles/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2aca7e39a72e0944d78b8fd956a321efafd92597 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/upsidedown_mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f149e6865869385309f9136a88edb9aca9cd1e93 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 128 lines last edited by Zeyu Ma in worldgen/terrain/assets/upsidedown_mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5dbfb9cd1ca123e3a7ec29af3f59560d692ee813 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/assets/ocean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f58de275dec6cb66488f2233c5560eab3374f395 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 90 lines last edited by Zeyu Ma in worldgen/terrain/assets/ocean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 18aa7286bdac54d96aca7c59d0132b90cc381412 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/elements/warped_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 82df81a81ba1673e4acc974c2720118f7b140e2b +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/warped_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3a85eb77ccf8ac81d26619cfda62911639670ad2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 43 lines last edited by Zeyu Ma in worldgen/terrain/elements/warped_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e7dcb0590764a47d85957510df5d7ebf6a41c2e6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/elements/waterbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c045a41b921b077a28270a7186578375f96fa48c +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/waterbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5686d3320b13e47eb68f4d0b2cc08383cb6ec4a0 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 37 lines last edited by Zeyu Ma in worldgen/terrain/elements/waterbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 87f50e89f6376dcd496bc68fbe6490541e480863 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/terrain/elements/voronoi_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 245a069e86f175305dacae906c93a80f27909bed +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/voronoi_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 62a3601780807f6c69e4a454a8b4901a12422ee2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 75 lines last edited by Zeyu Ma in worldgen/terrain/elements/voronoi_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 148a8c47d241443d5e4c21e20ed5f7b498f63122 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/elements/upsidedown_mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2eb5d3742b589846bc2f7371dd182178e8cc3e34 +Author: Pvl-bot +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/upsidedown_mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c8e6486f6b1cd5b8d14d2b966b19bf7177d69cf9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:38 2023 -0400 + + Add 74 lines last edited by Zeyu Ma in worldgen/terrain/elements/upsidedown_mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a82bee772e769b04e5329eb1a23cfbe335c0cf99 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/landtiles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9904aae6247d7f7b36d4fe7bf6a92ffdbfba2976 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Lahav Lipson in worldgen/terrain/elements/landtiles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a0e8810f5f19cf13e0070948c609ad0478d24db +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 197 lines last edited by Zeyu Ma in worldgen/terrain/elements/landtiles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b906721695d83abe498c1f4b868b93e6d375edcb +Author: Lahav Lipson +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/elements/ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1970501acbc38d12a59ff1c7ecd69f9c8ba02e0a +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eed0ce1a975dbba39d2551c290b670eaf42a974c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 55 lines last edited by Zeyu Ma in worldgen/terrain/elements/ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2b37eaec949520fbcac37e28fffa8cb1f4d84f07 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a47e4bfd4a7558d284efb4cca98cb49a9217cd87 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 94 lines last edited by Zeyu Ma in worldgen/terrain/elements/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a3a96cc43e10cec80ae558a0e6bcb42d927ee6e6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/terrain/elements/mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ce626ea179d5c281b4fb8453c91aad10ad72dfe6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9ec0b8ba4f80f1ec4b9700a5863705c927e48f7c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 54 lines last edited by Zeyu Ma in worldgen/terrain/elements/mountains.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 10fcb2ea95548b0e86d9b352b264094ce0861d3d +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/caves.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e654b23e528a6223bb1a004aa327a0989896d7d5 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 81 lines last edited by Zeyu Ma in worldgen/terrain/elements/caves.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0dbe126a05b2472768da60bbd79fe8feac9a4d73 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/elements/atmosphere.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2789b1150032c98e6f308b7345d0af9164dc3d18 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 29 lines last edited by Zeyu Ma in worldgen/terrain/elements/atmosphere.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b6f3902b21d6c42410b5ff0fa80ce27ca1d282d +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 303 lines last edited by Pvl-bot in worldgen/terrain/mesher/_marching_cubes_lewiner.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d7276722e5b2903d68054d3333c873c07b30dc2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/mesher/cube_spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit deaf37362c33b80a4554e813a221fb0b3078da54 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 221 lines last edited by Zeyu Ma in worldgen/terrain/mesher/cube_spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 658f2915ad5103a2c99fac1f8996e39768f31217 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/mesher/frontview_spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7cea012be68bb384ec51b175a65b8af904942bba +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 230 lines last edited by Zeyu Ma in worldgen/terrain/mesher/frontview_spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1179436a1a0d6f39459670af25f3a3c7cc2470fa +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/mesher/spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efee975ee0325dc661fd9a539a37efdd6dee0bda +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 159 lines last edited by Zeyu Ma in worldgen/terrain/mesher/spherical_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b01fd68abdd8ee470026217cf582bf9c5e4fa1e5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/mesher/uniform_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dbdd9e83ff4614d324aa975ed65ad5f94338e9df +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 154 lines last edited by Zeyu Ma in worldgen/terrain/mesher/uniform_mesher.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e90711721cee0f493ca1308fcfd3f0d3bbc57901 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/terrain/mesher/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c4e4e22831094f7fa6256653008d19e7e08bcfd2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:37 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/mesher/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e752f111b1c9f08400e7d3a50f0a9210843a82b0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 533 lines last edited by Pvl-bot in worldgen/terrain/mesher/_marching_cubes_lewiner_luts.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 925928586bbe9e22ff80ead4c125bfd25766020f +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/terrain/surface_kernel/kernelizer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a223624a858b5bc9ceb3a033ed13c095d87feec7 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 330 lines last edited by Zeyu Ma in worldgen/terrain/surface_kernel/kernelizer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4efdd49ef27e2895645ea5ca945b5d6f4eeeb6b0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/surface_kernel/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 02c0cdb1579e441ce826b4b7e7f9aac5de2e8a40 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 104 lines last edited by Zeyu Ma in worldgen/terrain/surface_kernel/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 697cd6a5b31739eb688ded342ba99083434e327d +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 1460 lines last edited by Pvl-bot in worldgen/terrain/marching_cubes/_marching_cubes_lewiner_cy.pyx + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9451c899d0e32f339311dc624a9dc5f380035fdf +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/terrain/utils/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff96bc4ea57c13d27fd0a3f7e0a556ce1d976557 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0bf4a35c1471ffeb7727e2bfed0b61aad65a5760 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 24 lines last edited by Zeyu Ma in worldgen/terrain/utils/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6ddc4d66a325bbf292efb639fe19dff6baab2547 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/ctype_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75bdaca30979d267e9016a9feb796b3ef0168b65 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 22 lines last edited by Zeyu Ma in worldgen/terrain/utils/ctype_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 074fec5a44e7c16cf8f58c48e1a5e28bfb6e60d8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/image_processing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a187f56bac34492050aec2ae7cbbcf299fe1fe5d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 69 lines last edited by Zeyu Ma in worldgen/terrain/utils/image_processing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4e9448cb137841f81d40e7f9c92d3dadabaf3c5c +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit de0ea76dc9044be143620b623ce4376355bdca4c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 17 lines last edited by Zeyu Ma in worldgen/terrain/utils/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ac59976ad3e99d982612bf2b56820191f5c230c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/terrain/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3e39289b52c0f3bd7caefc2c77ce59f2d5747ad0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 46679a09de0b50ceb0963188944226bf7eabf244 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 16 lines last edited by Lahav Lipson in worldgen/terrain/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4bee70645bea16cfaf421811b6b160902d18a8e7 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 302 lines last edited by Zeyu Ma in worldgen/terrain/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b809edd7070ce61c92bc50a31fb0ac840f8a104d +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 78b60bc0c8eb0e2dc086264fe04dee8481be0fed +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 71 lines last edited by Zeyu Ma in worldgen/terrain/utils/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b1c8f405bb5eb41c3cacdbe4bbc7b7586ad5ec5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d817eacf5d7323e52a6712306120479c899535ff +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 47 lines last edited by Zeyu Ma in worldgen/terrain/utils/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 583dd159742bccfefcfce05e9ca035501b12de53 +Author: Pvl-bot +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/utils/kernelizer_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f2986d5004265d9816f3710c6c12f5b7862e37dd +Author: Zeyu Ma +Date: Fri Jun 30 03:11:36 2023 -0400 + + Add 283 lines last edited by Zeyu Ma in worldgen/terrain/utils/kernelizer_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 06732fbabfdac9135c1b261573c4f1f2a34711f0 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/terrain/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ffaf7ae18a5a757554a233e74f0078742b7eab20 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 83fbc38b21a289cc8c020882b3873fcd0ecb11c6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/terrain/setup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ffdca236d98d94675bee9d12624b25dbb7126288 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 29 lines last edited by Zeyu Ma in worldgen/terrain/setup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e4527748bdb39f5da991062f99592e3cfc7669a4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/terrain/install_terrain.sh + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 117a5775adc9056ff410126e47dfc655e62d1b93 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 124 lines last edited by Zeyu Ma in worldgen/terrain/install_terrain.sh + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 118d889c100359b226a7447eafadb6bf3cab1418 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed02f2ca51e2aad9f9ce383642fc622bc026a0bc +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/terrain/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efe1ee2bcb899359be9753e333cb07669b74ddb7 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 456 lines last edited by Zeyu Ma in worldgen/terrain/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 42dfeab2c3d8ce8cf948409a786a20e0d6e61f3c +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/terrain/scene.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1c3caccc175b6035dddfa45085bb422e782337d2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 100 lines last edited by Zeyu Ma in worldgen/terrain/scene.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 63386fc3333b2d126c88dfa0d30adfbb939c2abe +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/util/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 012a8e9549fdc178ef54b7fafbff7012163d358e +Author: Hei Law +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 3 lines last edited by Hei Law in worldgen/util/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 839d5350a126b821a1834f738c713d0a5908eb67 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/util/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 46c0ea7bc2bc0c655febf0a6e51bb2a958cc6db2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/util/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21d8b9c7c45c004e70c56f9f6d4a7ef7740871f6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 56 lines last edited by Alexander Raistrick in worldgen/util/logging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 485d67bf71418bcf6b84f5d7d877227cbab4806c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Zeyu Ma in worldgen/util/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db832935d38cf9e633f567c1d3e9afc7e2a07c85 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3b13a4228c0febc2cc37af86c4ab2340555f3877 +Author: Jia Deng +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 44 lines last edited by Jia Deng in worldgen/util/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e02cb8505fe154dc706534e7469373adeea196c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 52 lines last edited by Lingjie Mei in worldgen/util/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 908ea180068a6204f3dc13151847caa8bc328640 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 267 lines last edited by Alexander Raistrick in worldgen/util/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d33c4aed5aa4e63f3c5ce5688726750bf00af072 +Author: Yihan Wang +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/util/exporting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3387103219d28ac10da49b585262525c9f71db18 +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/exporting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dc7a834e6a6707c577fa4312f864888ecbcde2bd +Author: Lahav Lipson +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 250 lines last edited by Lahav Lipson in worldgen/util/exporting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 61fcf50d3134f9d22e9e1c62f75c85bed755dc7c +Author: Pvl-bot +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d00d862971805fcc66043c69ecec72a27ceff2bf +Author: Lahav Lipson +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 7 lines last edited by Lahav Lipson in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 71d750b18ff6207fbfa5e1a055588239a90f7176 +Author: Hei Law +Date: Fri Jun 30 03:11:35 2023 -0400 + + Add 10 lines last edited by Hei Law in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 73ebc49214c52725faae5e62a8d66b7330c2c273 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 78 lines last edited by Zeyu Ma in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e36706d04f7315ed8c71293bdc23c9680a7a12bf +Author: Lingjie Mei +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 84 lines last edited by Lingjie Mei in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cedf84ffd431bd56c0422d4964c645870f48f569 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 553 lines last edited by Alexander Raistrick in worldgen/util/blender.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit edbdee1c56915247227fdb1f45f0413f8c70606b +Author: Pvl-bot +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/pipeline.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cfe923609e0358d3e94ff5bb0578cdcb13170de3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 67 lines last edited by Alexander Raistrick in worldgen/util/pipeline.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f6deec15fd555fa4948ce00d8987bba29a32c908 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/util/organization.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8a5819bc7935e607d8f3facc87ffdd14f0cc7707 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/util/organization.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ef4b0109ffb30d1eff7f3ed7bff63391eed235a5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/organization.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b1c6b86028c3c9373953fcf34b80cdfb1e058312 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 103 lines last edited by Zeyu Ma in worldgen/util/organization.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c88e81bc425ae41b69c9ab2accba3d1eb7ef704 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 175 lines last edited by Lahav Lipson in worldgen/util/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0f1cd7c27afe1c40135b3773b85b84bec7cc608a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 1 lines last edited by Lingjie Mei in worldgen/util/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 36d3bd1b856af1f97fe3d80fdb8365f51a72926a +Author: Pvl-bot +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/util/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1bc8e3097c5d2322590ccb5e8d0aa553a318b55a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 59 lines last edited by Alexander Raistrick in worldgen/util/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4482ea1e124e29af7055eb25c5117615fa7e73c1 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 134 lines last edited by Zeyu Ma in worldgen/util/random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40f54d04f4ee7153de1fd7ce97d716675d165dc7 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9addba12191b7491f6625bf327429912c7ab26e4 +Author: Karhan Kayan +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 4 lines last edited by Karhan Kayan in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 70890ea719a72686a870d3b8f4bf262767235869 +Author: Pvl-bot +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b928dc6010d30bc6c15747502a610084550d957b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 9 lines last edited by Zeyu Ma in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit aa111820262e700b218baf3a20b9113434d70168 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 17 lines last edited by Lingjie Mei in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dc2b197c51c7c3b5046c8ec42bbf5a3f27ddebb3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 218 lines last edited by Alexander Raistrick in worldgen/placement/placement.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2cdaff645022d47008f4cde8dec1dc44d3441f3e +Author: Hei Law +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 1 lines last edited by Hei Law in worldgen/placement/factory.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 792c0428da3a0ffa35c7e7515e74b39c933ee5b5 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in worldgen/placement/factory.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4725bb330bf9dbbb6290ef135d806e877f141c0a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:34 2023 -0400 + + Add 7 lines last edited by Lingjie Mei in worldgen/placement/factory.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ca08588329fb15f13c42befddd68b70adec3cf59 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/placement/factory.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a41cd76bbead9802f8b7764897357d442ed26290 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 146 lines last edited by Alexander Raistrick in worldgen/placement/factory.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b82018bb812f887ba9d22a7a513e2a76dccdf228 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/animation_policy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b87800c9c9d11a9b2feefd6e4fb92b75e2926d9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 32 lines last edited by Zeyu Ma in worldgen/placement/animation_policy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e977a20cd5004d0850a73e800b44a13299b94418 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 444 lines last edited by Alexander Raistrick in worldgen/placement/animation_policy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9243b65884d9e25347629d9b1e11adac9d7bac19 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/instance_scatter.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b7cf1454c112188b131453590bf01919b29623b3 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 12 lines last edited by Lahav Lipson in worldgen/placement/instance_scatter.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 867e6ea2fd400e3d4416fb91bb0bd285f8e66d9c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 35 lines last edited by Lingjie Mei in worldgen/placement/instance_scatter.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7d37d055539cdbbfdb91f50d687bbe6e0a845e61 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 169 lines last edited by Alexander Raistrick in worldgen/placement/instance_scatter.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 53783ffe6884f472186f20c629aed9c913abd1a6 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 3 lines last edited by Zeyu Ma in worldgen/placement/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 36c5a8aeb8d22716c37b2fb767b37c61d0e3d224 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/placement/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0857ebfe739496198467dfd82bcdb1dff53c8472 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 18 lines last edited by Lingjie Mei in worldgen/placement/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 57aa91c3de91726209722c65e3f76c91f9f84f49 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 208 lines last edited by Alexander Raistrick in worldgen/placement/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4107232a4579514b6c502af3f576f21a45491d42 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/density.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fdb9d66f2cd2feeb97289905a5d8113f048c0c51 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 8 lines last edited by Lahav Lipson in worldgen/placement/density.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 10f4c392a2469bab06ee89e990e465182629aa9c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 8 lines last edited by Lingjie Mei in worldgen/placement/density.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2153c9e08391a886d0f62707b3393062de2439ed +Author: Zeyu Ma +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 25 lines last edited by Zeyu Ma in worldgen/placement/density.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f625ea0e99b4b94c665e3092e4cf01102f36ffa8 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 32 lines last edited by Alexander Raistrick in worldgen/placement/density.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a34b388cd196dcf53fc4cd9d63477a6cca75f229 +Author: Pvl-bot +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6827237a6d35bcd3dc04023f17fecf06afb2bd4c +Author: Hei Law +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 20 lines last edited by Hei Law in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9a90db1a9b11d5fb59cdb26748eba95ace99c412 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 38 lines last edited by Lingjie Mei in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e79b1f08b062557304a40e18d081be543cd4a366 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 89 lines last edited by Lahav Lipson in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a87dd9dd313e6afb6cbce291048481cb1631aa73 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 92 lines last edited by Zeyu Ma in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 23aab7f49dc8cdfb5779bc438bfadc80a48f6895 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 288 lines last edited by Alexander Raistrick in worldgen/placement/camera.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6988922bf8f4c0cba386e9770ede022b888feb0a +Author: Zeyu Ma +Date: Fri Jun 30 03:11:33 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/placement/detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bfbcffc4d4f3c9b3133f93b0ce5a9dc070b08be2 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/placement/detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 02bb2d21b84e2325f95de075d65fdc0b8f171a11 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 222d9c1fab7608bcf17ebe071e78e6c8870073d8 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 64 lines last edited by Lingjie Mei in worldgen/placement/detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4b72f3a4998779b59f6f3f0cefbf368d262d388a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 99 lines last edited by Alexander Raistrick in worldgen/placement/detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0b8a294a27a82e9f77b2be5770411bc56bdc6f0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/placement/split_in_view.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 82c342392565d7f1737ff7d3c9e19a0675528903 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 115 lines last edited by Alexander Raistrick in worldgen/placement/split_in_view.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6d9da80880f95fc0ed6365ecc0e1ce725ba04a61 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/aggregate_job_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 35a0f09e5f97c8a45b2c176dfcad2b583742ab53 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 95 lines last edited by Lahav Lipson in worldgen/tools/results/aggregate_job_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8676d57777c3c4bbf834e7308c8596a37b9613ee +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/resource_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d2f8d568da167478ea6077dd71d539621b7a73d6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 117 lines last edited by Lahav Lipson in worldgen/tools/results/resource_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8892d082d732f50dc09404a13f715dcc35924cc2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/job_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fddf01eafb2302d2c519cf4ede579dbbf9ba6665 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 99 lines last edited by Lahav Lipson in worldgen/tools/results/job_stats.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb3127fe990079687a2572150804f1fe3038d899 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/scatter_figure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 49e3cc0dcd6588d77ba6b49e550af03b46ed8cab +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 10 lines last edited by Alexander Raistrick in worldgen/tools/results/scatter_figure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 92af756e27ca7d059730d7e23617d62294a4d035 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 170 lines last edited by Hongyu Wen in worldgen/tools/results/scatter_figure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit de67aa7d9db88ae0455da5b85dcbbf3e676e6c37 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/make_grid_figure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3ca2c27b14b2f216a345a48b95a2ea4cd8ab3fed +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 79 lines last edited by Alexander Raistrick in worldgen/tools/results/make_grid_figure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9498fc5f10bef531cc42ae8192345d53435284d4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/tools/results/parse_videos.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0103dd8bfa036961e06a83cb637d3c34cdb948a3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 47 lines last edited by Alexander Raistrick in worldgen/tools/results/parse_videos.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9b8ebdd6d0ff94d1dcaf6342794fd69b8f8acb81 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/tools/results/parse_times.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cfabaa0e5382f37a3e3dd21db3ea656c7d630e20 +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/parse_times.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 79acaca0fe22868c148f7b139aabdb93d7feb701 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 53 lines last edited by Lahav Lipson in worldgen/tools/results/parse_times.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8212edf7ba8c650617483342277cbef525b3bd0a +Author: Pvl-bot +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/results/strip_alpha_background.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a5ef91f137828efb45b31a1ea0458d12063cb40 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:32 2023 -0400 + + Add 35 lines last edited by Lingjie Mei in worldgen/tools/results/strip_alpha_background.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6758b0a8aa2e37ab02b26f30c927ee1c7306f50c +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/tools/util/smb_client.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 670263fa5626ac943e41232ae0e033323861d207 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 31 lines last edited by Lahav Lipson in worldgen/tools/util/smb_client.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 94d33bcfc2bb0f468e7d83728f6be9a258f44e7f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 40 lines last edited by Alexander Raistrick in worldgen/tools/util/smb_client.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 978c2060b01741bacbea1d7677e34309fd55cccd +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/tools/util/submitit_emulator.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ad1a2cef2e995aaa6ce7584ebbdae1c74f11eeca +Author: David Yan +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 13 lines last edited by David Yan in worldgen/tools/util/submitit_emulator.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bed7df582a34fc21ad31bb5bbf45e570338c4e9b +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 48 lines last edited by Lahav Lipson in worldgen/tools/util/submitit_emulator.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 00182ebfe08685c4f032d47ae5a9a08c4147477d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 205 lines last edited by Alexander Raistrick in worldgen/tools/util/submitit_emulator.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ebe043ff1affb0125f208cdf8046aa17fc184ae2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/tools/util/cleanup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0d078c7189acf2031d15e43e5f8b53a39b387a1 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/tools/util/cleanup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6227474d3e683453f0fe763abf823c4d495af2f9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/util/cleanup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4fce49c6f76d6a1e5829dcab4c33cfff8fd4ad2c +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 26 lines last edited by Lahav Lipson in worldgen/tools/util/cleanup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 310bd59de24a6a5b929b922c58e6f00b6f481393 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/util/google_drive_client.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 25c6636d75cde3d067455f39683c08fd9bdf76e8 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 17 lines last edited by Lahav Lipson in worldgen/tools/util/google_drive_client.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8780d296a177e91f12d430d7afaeff7d44c21b33 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/tools/util/show_gpu_table.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ee6e23b85481c92b55a899c90ab785c95874aea5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/util/show_gpu_table.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 82cf3acaccda4a67b013d3f8ce81fbcff59bdec3 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 58 lines last edited by Lahav Lipson in worldgen/tools/util/show_gpu_table.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e03f4ee23839c5b2ccb1886cf90139536a3b1f31 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/util/upload_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit da47341be52fa9269168df2b18e03917b2d9ea71 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 114 lines last edited by Alexander Raistrick in worldgen/tools/util/upload_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d4204525783da9944de8b324abf6d55dea32000c +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 3 lines last edited by Pvl-bot in worldgen/tools/ground_truth/bounding_boxes_3d.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fb1c6062e6854da6e2b1897ff9e948a32fca1dac +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 128 lines last edited by Lahav Lipson in worldgen/tools/ground_truth/bounding_boxes_3d.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bf2e16cf84ea1767fe2db5aa899043cea6537814 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 3 lines last edited by Pvl-bot in worldgen/tools/ground_truth/rigid_warp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b5820c110a64179c3ee4471e2584de19379ce75 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 71 lines last edited by Lahav Lipson in worldgen/tools/ground_truth/rigid_warp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 14356c73528dbd25dba55cf1765b0d20aad231b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/tools/ground_truth/segmentation_lookup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7bd87435b3d3f351264cc26964fe97f89c19652d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 112 lines last edited by Lahav Lipson in worldgen/tools/ground_truth/segmentation_lookup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b6339eb7e74a287d9f942ff110d5e36396b2ef1e +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 3 lines last edited by Pvl-bot in worldgen/tools/ground_truth/optical_flow_warp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b4969f7d0d6fbfe4b84668934de01f8b7a24ce49 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 47 lines last edited by Lahav Lipson in worldgen/tools/ground_truth/optical_flow_warp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7047e2ba8c5e2e25bafd8f53e58bc8bbdd3c1e61 +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/tools/export/export.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd713d252d8d5bc99373f9386866c5c86e81db22 +Author: David Yan +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 298 lines last edited by David Yan in worldgen/tools/export/export.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f52a24db5021621de0dad157af6737d7d2d1568f +Author: Pvl-bot +Date: Fri Jun 30 03:11:31 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/dev/params_parser.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9eaf36ec6d0674095af00377d974119c5c22a449 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 43 lines last edited by Zeyu Ma in worldgen/tools/dev/params_parser.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5267ee392fc6acc14c402a7e756d4bcac9a96c0e +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/dev/landtile_viewer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9c03b35df3cfde7422288acdb681a9857f99aeaa +Author: Zeyu Ma +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 34 lines last edited by Zeyu Ma in worldgen/tools/dev/landtile_viewer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b63d362e30570d4a5c675ba7d47ff537c630cdf4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/palette/palette.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1e5d6b7e6490175774557f5d1f49eb866dfe20db +Author: Lingjie Mei +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 12 lines last edited by Lingjie Mei in worldgen/tools/palette/palette.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d1df11c61f005f64584e618fdcb719d62ecb2412 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 99 lines last edited by Zeyu Ma in worldgen/tools/palette/palette.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a363493017d476d8f550a7a75bf839dd411ea721 +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/tools/pipeline_configs/local_256GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2722260c66efa202ca74c954125239a2c0813d4f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/local_256GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f38f050b7e7b991112dc70b36bcc1f1d4d6539e8 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 35 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/local_256GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6977eab608a3cfdb46ad724d96f29a34be40e16e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/local_16GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 37005f39db0103e8978e6da2d171e56851ebfb07 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 4 lines last edited by Zeyu Ma in worldgen/tools/pipeline_configs/cuda_terrain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6eb9fcb89bb62684144b2e2559f94eb159831b30 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/stereo_video.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fda5805ac8684eeb2ae851647fa7f528e0f59db6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/local_128GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 26fb0a355b8746136af96fbc07c0fc3ccc9fd4fa +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 3 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/asset_demo.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c3ce2900eff969e39e074407efa5dc69d9ee41b6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/stereo.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 718eb794a7288c1954b9c6b0e5114209a9ce5be4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 10 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/stereo.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b6670a890627c407b96178ae62b68cecc4b7fcd4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/local_64GB.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b55b251cca8b02c58085649023533f30f9828d7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/tools/pipeline_configs/monocular.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 45a8d71ea618286cea9bf2b70a62dd2208ca4bbb +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 8 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/monocular.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f2ca8ada5a5d4b6ef6dbe00bc3433cd43555a7fa +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 8 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/monocular.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6901c26a95dabbe5c262f019aa0629c6f146eb95 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/blender_gt.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7655583efc525fefecf5e326dc69458e28dafea1 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 3 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/blender_gt.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7254ce423eb9ebc13c9277d265ab55be18f2e53f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/monocular_flow.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b005f8b93e7e17a5ba682ac99c645d1dd1756c96 +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/tools/pipeline_configs/slurm.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1246ea48a4a670612808029109ae3313e0507c29 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 28 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/slurm.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 73fbe7c44af1b2b4193fe4a0268ac321e45e89f9 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 58 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/slurm.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f84978fecdf46041a66ef24f9649f3e249171f2f +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/tools/pipeline_configs/stereo_1h_jobs.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db91f830c86eaaf97f370abdfad01ec604dd96b7 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/tools/pipeline_configs/stereo_1h_jobs.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 633614af2f0d3673002442254964621cf92660c5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 11 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/stereo_1h_jobs.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3e7de65403621102303402d84a2af89413a96712 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/tools/pipeline_configs/opengl_gt.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b3879ee532a6b98924857a6d0c8b46f8aa8bd32 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/opengl_gt.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa8189908a59a007b2f664e96f5d4fc3e1ee346a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/tools/pipeline_configs/monocular_video.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b347c94a3ced3313023ff91e4adcdf5ea0816898 +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Pvl-bot in worldgen/tools/compile_opengl.sh + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4d9d863fa804367782efa435b6886d0135e608fb +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 10 lines last edited by Alexander Raistrick in worldgen/tools/compile_opengl.sh + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd1a01c327668961df34fed88a12b019a755cb2f +Author: Yihan Wang +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/tools/asset_grid.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c05865f99ceb29f2f2a088fa5e95d86945a29f9a +Author: Pvl-bot +Date: Fri Jun 30 03:11:30 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/tools/asset_grid.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85d63a82a9eece6ab070ad0d1eaedb7d3639ae0b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 118 lines last edited by Alexander Raistrick in worldgen/tools/asset_grid.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8cd15f0f5bd16dac9b313737ff08ccb964160f92 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 204 lines last edited by Lingjie Mei in worldgen/tools/asset_grid.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f96cd6186ef353a841dc0ffa8eddf548e9d8af3f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/tools/generate_terrain_assets.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c108731140abe296846a0bd2b6989da921c1dbea +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/generate_terrain_assets.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1a6004970fc585412f492ec2ac2d12511a5c5c29 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 76 lines last edited by Zeyu Ma in worldgen/tools/generate_terrain_assets.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85b90a55e816bd19f2f33b037d6a82446295325d +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/cancel_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a55d2ae7f0210e51653f9c494e49f06549142f9f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 22 lines last edited by Lahav Lipson in worldgen/tools/cancel_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 809fa9e8ee14c5836981c9b702601b86617e328d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 23 lines last edited by Lahav Lipson in worldgen/tools/template.html + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 733da481d61a73799405cba6fa6f8b2ab934db10 +Author: Hei Law +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 37 lines last edited by Hei Law in worldgen/tools/template.html + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c5a03edf6c52d7c4bd535ec06eef81787d2e4aef +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/tools/kernelize_surfaces.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1305f23f35c2388d7dc4deca2808bf6a5ad01ce2 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 30 lines last edited by Zeyu Ma in worldgen/tools/kernelize_surfaces.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 25f237ab3d162d4cf88e40c8b9a8534e664ec721 +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/tools/summarize.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit de86cd7466eceea4ebe7f8198482e05e0f04d510 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 151 lines last edited by Lahav Lipson in worldgen/tools/summarize.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c737f78639f7a6a0134f9603023400aa463431e7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/tools/manage_datagen_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97f5bdcb7ab58d3ed669eb6f5eed289a607b0f1b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 34 lines last edited by Zeyu Ma in worldgen/tools/manage_datagen_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 84fbb516c6136cee72dbb65c5f01109a9be596e0 +Author: Hei Law +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 67 lines last edited by Hei Law in worldgen/tools/manage_datagen_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1327421eec2e8ed6d9be6bc1142275410fd18c8e +Author: Lahav Lipson +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 212 lines last edited by Lahav Lipson in worldgen/tools/manage_datagen_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1413fd14f328c843f73e19661c923c2afc46f5db +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 719 lines last edited by Alexander Raistrick in worldgen/tools/manage_datagen_jobs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 41ce5efcc16b7e95e9b1bd330d1ad4e234eb349a +Author: Zeyu Ma +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/grassland/grass_tuft.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a6367dcee3d7fa1461434b966a017d7734938607 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Yiming Zuo in worldgen/assets/grassland/grass_tuft.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 828739a7f06bde25102962087a49ea44fc9d1e1b +Author: Yihan Wang +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/grassland/grass_tuft.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f8fafdd121a45b34dc08c7fa097313f6ce30d51d +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/grassland/grass_tuft.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit add953855b65019cbed6c00a1310040d6e0641e2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 88 lines last edited by Alexander Raistrick in worldgen/assets/grassland/grass_tuft.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff4c7ab90be54e90df9266286711cc20ca2b9375 +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/grassland/dandelion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 712811a3c6fbbcf572423c41bea6ca5214227f16 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 11 lines last edited by Yiming Zuo in worldgen/assets/grassland/dandelion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d1aa204da9ed93f71d0e3a99572c91c78359c5dc +Author: Yihan Wang +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 17 lines last edited by Yihan Wang in worldgen/assets/grassland/dandelion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bd246c44612d31a5a073bfbff7192c4a238b4410 +Author: Beining Han +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 636 lines last edited by Beining Han in worldgen/assets/grassland/dandelion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 57acf128c6b1e9d287bd01d7b6bfe7d9d2b193cd +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7bb69c857227dfbf66f6c6442c731aaf17c69e97 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Lingjie Mei in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bb2a5c61e220dcac04f081f1311ba1778f5ba09d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2073684656f6a7801bf8b20fcf317ceefeadb645 +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 091332d8dcff3e3c9472c43238f99f9c9fd34501 +Author: Yihan Wang +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 23 lines last edited by Yihan Wang in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 144978fa943144734c42a0386f5b034614649b86 +Author: Beining Han +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 578 lines last edited by Beining Han in worldgen/assets/grassland/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59adba075dd6c0cf41ca2bdf81d55f23a2a01456 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/grassland/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 96c7d57ce224df624fd1d7c6f9349263f1c48a52 +Author: Beining Han +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 1 lines last edited by Beining Han in worldgen/assets/grassland/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21b90d6d5203163e8455a54dbe096561865ab448 +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/grassland/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 55987196f3c41271d99923de018ba1896acd89cb +Author: Pvl-bot +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/coconuthairy_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f05e8ccabd61417ff7656c7bd552d6600bec9a9c +Author: Yiming Zuo +Date: Fri Jun 30 03:11:29 2023 -0400 + + Add 80 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/coconuthairy_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e10f35a199be71126a912e2207c458f35d2a74be +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/blackberry_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85a762235d8fbf7a95f65044c44d97080550fb90 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 107 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/blackberry_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 54b57ced9e69da6a65b084df4c373afcd6da480d +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/durian_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 143adc3d8129058ed8f88fc5499a4e3bc9ff36d4 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 110 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/durian_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 354fbaef665951117680bddfd77e788eb7678167 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 88d40e6b151640a53ed44da426a6ae8738a34900 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 50 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7188b16e740fb10699bfe15cfda96109a5a89cb +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/apple_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ecd0738a7b338a60f24793f57d8292fe3a28ef63 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 85 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/apple_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0c2e13ecf8a5ed27f47f028e0975aec9fa60c33c +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/starfruit_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 789d38e7c8191649916f1f8ee96cae1f75717a45 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 76 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/starfruit_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56bb6cb18fdea93c3fcc4c0973bc9d5a5ed549a9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/strawberry_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ef536d3c0b83c3354e9d6abc7e9673d76b34ad59 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 96 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/strawberry_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 926da6b1bbf98cebd6bb37d87a1d79892d67f85c +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/coconutgreen_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cc2bcdcce487c9e4cadbf98f2a2d0567d2038d40 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 93 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/coconutgreen_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9dd49b82f4c618cb7e0ec17011ab7452f1270656 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/surfaces/pineapple_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 83c40183fdfb0dc8f920f3547b056d2299da9e64 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 174 lines last edited by Yiming Zuo in worldgen/assets/fruits/surfaces/pineapple_surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dce4a7271d06275ae92315a38783146f07428aab +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/compositional_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ebb4f2d5748bf6959d785e0777399c2941b1bd27 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/compositional_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 796df833c2f17e3c21b26aff9b21ed9d4d5383b3 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 61 lines last edited by Yiming Zuo in worldgen/assets/fruits/compositional_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dbbfb420c03e0586eb9dd7cb3bd82af509307b03 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/apple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1792c3ae3dbfd2b98be0993731b9db30972c5f94 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/apple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e6a18486386c20c683c22e736ef26657f5c05860 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 92 lines last edited by Yiming Zuo in worldgen/assets/fruits/apple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b9e960c9392b2f1368dea76a31b855aff04412a4 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/strawberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b0d54be1d52590d2c3dc17e0b91c70604c30d022 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/strawberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cc5cf22a982167d0932f1851a88f9e988cb9c8ef +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 95 lines last edited by Yiming Zuo in worldgen/assets/fruits/strawberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 81fb223ea7a93ba2b43bab7bcc02f1276bab8368 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/stem_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 29988239c302a2df95f36fd3a03ae8c92b633d5e +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 617 lines last edited by Yiming Zuo in worldgen/assets/fruits/stem_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d78bc6b2da1b8f7a2a28b55a6cad1fb8cfcba11d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/fruits/coconutgreen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b3d5199277dbbc68aea57b25b60e2734079ef028 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/coconutgreen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b41d0bc8ab60335da71c05ae4e541cd23208c1b1 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 100 lines last edited by Yiming Zuo in worldgen/assets/fruits/coconutgreen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 64a47ed0b0a1528e6e6e4cb97798fccef1e41693 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/cross_section_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2761b2043f61865997b484ef93f671302fd194a6 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 251 lines last edited by Yiming Zuo in worldgen/assets/fruits/cross_section_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 68a799e8f9ea4528cb6d6affa8b953528a6d0b6f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/fruits/general_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c308a98a8d398638e03002eed9740d2433ee8c83 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/general_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e0fa15e74ff68710635eabc1d78c3227598d5d7e +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 167 lines last edited by Yiming Zuo in worldgen/assets/fruits/general_fruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d2cdf77edef61f136f73c9b942f8ba9bb00f9b87 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/starfruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 930803251ec0ec00cf2e4103912bf965923afa57 +Author: Pvl-bot +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/starfruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2fce798df6aa40980b7813a5b92106c020831009 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 85 lines last edited by Yiming Zuo in worldgen/assets/fruits/starfruit.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6efbcb3cafcf4eaba9730960ef71ba6999afab4e +Author: Lahav Lipson +Date: Fri Jun 30 03:11:28 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/durian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a9223b901fa0b4dd288341c489886ec4e8dd8388 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/fruits/durian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b2d08340b40744089e6120290cdd4731d6f24c5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/durian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ec52cedb338f51d9625b3e8c79125764c619c4d8 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 92 lines last edited by Yiming Zuo in worldgen/assets/fruits/durian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c47928124f932564136750aecdbedc0f389e6fb7 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/blackberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bb77306aa20424e6f1c201577a3612203140ec7c +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/blackberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b9cd9c0d6ea3ddfbc70d722b47eecb46e8ab0fdb +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 74 lines last edited by Yiming Zuo in worldgen/assets/fruits/blackberry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b4fd1a69b1b67cbdd27344ee3c91013b8105a3e1 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 9 lines last edited by Lahav Lipson in worldgen/assets/fruits/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3f0ea4da02f6f5ca1de6ae420a51888c33d8ec6c +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/fruit_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0e26afc719509198ec25a9ac239c4d987ab36a2d +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 690 lines last edited by Yiming Zuo in worldgen/assets/fruits/fruit_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a7193831a907b9e99605f64e768e16e8f40a3c30 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/pineapple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e29aca0f3a1dac6c1f858332378d96b567b371e9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/pineapple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed7708823e60dba77264fd8d6d1e0c7c53247218 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 107 lines last edited by Yiming Zuo in worldgen/assets/fruits/pineapple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2db6cae024b6961cdd16b302b0610b7892e71e48 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/fruits/coconuthairy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 15f1215d9c5a4715e4b94f1aa635b86d8533149f +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/coconuthairy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b97b5231595d2adae686ba10d10734ff5baddba1 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 72 lines last edited by Yiming Zuo in worldgen/assets/fruits/coconuthairy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d541f3af606a07f033dc6d61f54f65bf7d50f11d +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/fruits/seed_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 64df98d039152ea22dfc7be40a1bd4eec0ec25c8 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 59 lines last edited by Yiming Zuo in worldgen/assets/fruits/seed_lib.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e724e581221d3ff9cf150a024df95762fd49d2ab +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/monocot/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3c0da77a618867de1b1870b5ae75665ecd58b3a4 +Author: Yihan Wang +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/monocot/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 63cce9574e3ca21dd83062fbe0dd5bf64d89b415 +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1ba7b24aabb153cf56268c4a420a67c0667d0221 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 205 lines last edited by Lingjie Mei in worldgen/assets/monocot/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8450419fad8e4dd8817de1c0f480aa7dd26ac2bf +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/grasses.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af3cd6323c01e9e5362f17dec6f6f9ceb21693bb +Author: Yihan Wang +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 9 lines last edited by Yihan Wang in worldgen/assets/monocot/grasses.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40bb71254302061337277493c354c3acd9849d9e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/assets/monocot/grasses.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6ecf0e37dc61cff2eb1ba4fdaac0db5983dbaa8f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 230 lines last edited by Lingjie Mei in worldgen/assets/monocot/grasses.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 95ead79fca144948b6b969e1caf36d50eb68f0bd +Author: Yihan Wang +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/monocot/banana.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c6398a72af0d0b755f2a015024a5e0cfcda878a +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/banana.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb79f5963cef9a6b737d63c4cdef2ba1a76012d4 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 142 lines last edited by Lingjie Mei in worldgen/assets/monocot/banana.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 93f5018ff8a860c8c22783ae476db9738f0243cc +Author: Yihan Wang +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/assets/monocot/tussock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d27e31e92e5f54115fd326e63c17801865ea76bc +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/assets/monocot/tussock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8fd0ad437e32695dae8205803ac9f79149597f28 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 35 lines last edited by Lingjie Mei in worldgen/assets/monocot/tussock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c71b7a99cb2b23516412b21bed90254e3a023163 +Author: Yihan Wang +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/monocot/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit edd205084b2965341d53d5b294459aa3ac529384 +Author: Pvl-bot +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e733ee4e9a08d847ea9e9e419bc84b6f43b34247 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 71 lines last edited by Lingjie Mei in worldgen/assets/monocot/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5225ca0c83fe9bf3a31b45111ad09f8b501bd317 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:27 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/monocot/agave.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d9a4ed3566d81085994f951fe82937b8d49508d8 +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/monocot/agave.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b9de7516790066ba137970f71b45966451172ad +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/agave.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 358d1897e7e29c2ce2e014803e0132936a68834c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 61 lines last edited by Lingjie Mei in worldgen/assets/monocot/agave.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5e210a4bbaee48f72945611fb005ddfa6e26a30e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/monocot/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 120ab6094f31b89095e9a003147779afbc6ae691 +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 5 lines last edited by Yihan Wang in worldgen/assets/monocot/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ee97a0163e460c4836c319f6a82b486f5a827fdc +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac47111069623844137010981b2aa0ee624b8919 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 46 lines last edited by Lingjie Mei in worldgen/assets/monocot/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 823b2b3cb5ffe1f4ab7ff80ec62ea050b60853cd +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 9 lines last edited by Lingjie Mei in worldgen/assets/monocot/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 22a8889823a4ccfb722f8d364f4172027e8ce655 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/veratrum.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4a9520c4a5e3c821043bcc33335f836cf2ac0f9c +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Yihan Wang in worldgen/assets/monocot/veratrum.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 202aa5ef533e4fcc49e51874b8e6fc45e2b735d5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 128 lines last edited by Lingjie Mei in worldgen/assets/monocot/veratrum.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 086f2b3fe6e89ebc93605065c472e830c1970488 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/monocot/kelp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 77cf40d1ed21799c81bfd023746739b0df69951a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 105 lines last edited by Lingjie Mei in worldgen/assets/monocot/kelp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb0a5777adae2fbb14c6a85b4ee66aade8c3c1b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b8443658824e220f4704d0914309b0dcf0b2cff8 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 20 lines last edited by Lingjie Mei in worldgen/assets/corals/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7baafb8809b00a613907c6b3f36b76afc5b82b5f +Author: Zeyu Ma +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/corals/tentacles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b836855c893889df12e733e9d57ef6cd8dd76400 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/corals/tentacles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit aeb1e5abaa3b3ce63a10a50c288fbe54d7989103 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/tentacles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0df25b19304f9b25f8eb6f8d063079f4000b90b5 +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 9 lines last edited by Yihan Wang in worldgen/assets/corals/tentacles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bd5d82978059c8c2e2131998d1ec4d6278136922 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 98 lines last edited by Lingjie Mei in worldgen/assets/corals/tentacles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2430ef77cc089464e1db93be37a60fac0ee2340a +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/assets/corals/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 99d0edc25a856aa8a1eca03f3858137f2dd3f46d +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 87ef8b6293076cbda62cb10bb36de64692d20257 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 18 lines last edited by Alexander Raistrick in worldgen/assets/corals/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f487484b64de38ca04ea8bedc143072cec1bc0db +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 154 lines last edited by Lingjie Mei in worldgen/assets/corals/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d366eb2995295a1b8fcdd8d84d194921c0549e25 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0ca98d8599378d01d9bce66d280a08dc81f1022 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 12 lines last edited by Lingjie Mei in worldgen/assets/corals/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ef526f03892433c06296bf94d5e5c0a826729813 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/corals/elkhorn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f8a161adf5d4faca7cabe4b4b8d2ae398482d3ca +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/elkhorn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b9feefd48601e7f2fccc1bd36841ea327878f8e1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/elkhorn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4f1affdb87a5ac91c1ec8c7d7050b4a8b508ba39 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 136 lines last edited by Lingjie Mei in worldgen/assets/corals/elkhorn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1c99e21c5edb613d8db4b3e3f79cf8f023e0ec77 +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/diff_growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1a3befb5c73e5cc0337c7d2d592310520f7e6c6b +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/diff_growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f4ca02e7dbc2c4f41a14a40b4e92782cf7fdb033 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 107 lines last edited by Lingjie Mei in worldgen/assets/corals/diff_growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f4890828f821f03c7278ee69b08f58f75ac7951b +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/laplacian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fe9718b723bfb85cb6d595c666e37438f7c772ae +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/laplacian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d4e9d38485761a33da40d58aab73965ca35b3b21 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 23 lines last edited by Lingjie Mei in worldgen/assets/corals/laplacian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0bdd6ba6bb04a7c23febd97d8fb7b9349502251d +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/reaction_diffusion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0862d438705880bb0f3849216343a4f48837e9c8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/reaction_diffusion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a3c6b991fb1cdfb77be56b85494c72a505e3c8f5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 74 lines last edited by Lingjie Mei in worldgen/assets/corals/reaction_diffusion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c29c069f271cbf4c180eb10aa0761fb619f1602d +Author: Yihan Wang +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/star.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8fc60f2386c23441a542fd1c662d5d05b57ec137 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:26 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/corals/star.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85a5fca6bae065e4a84375208d9aa9f4380ed5ae +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/star.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fb0707a3de2a0dfb0665ab545c0f4e7b6cce73d5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 116 lines last edited by Lingjie Mei in worldgen/assets/corals/star.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa5f252d4118e1e32d8cd3a89c42afe8dd019b0f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/corals/fan.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f3e7f10e24a07ae89ac2ec5396532f6ce4ad179f +Author: Yihan Wang +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/fan.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2d938f99942cdbf13033839f217ee6649dd3d26f +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/fan.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c76673033539398d1cfa0e4608ac0888dace150a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 83 lines last edited by Lingjie Mei in worldgen/assets/corals/fan.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 566cebe2ac0e5e887698f67cd7d821ea48faf482 +Author: Yihan Wang +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9ab68eb97050c2e4331efc00beab0bb69c857438 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ca13fbedd3fb48e34c419586c035746558ac86a5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 149 lines last edited by Lingjie Mei in worldgen/assets/corals/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 17a9578dc8ebc7aeb5bf5839faef4e435ce93c0e +Author: Yihan Wang +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/corals/tube.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8b8d932042f6b7edab062aad294b964a9e470ee8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/corals/tube.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff3c13348e33fbff34b78be45d758bc1179b4168 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 71 lines last edited by Lingjie Mei in worldgen/assets/corals/tube.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a527cdd371526df7c27d56679c66323e8c91684 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/utils/materials.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9daf24f322ecf3fd60bb4a1307c21504fbb22796 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 245 lines last edited by Alejandro Newell in worldgen/assets/trees/utils/materials.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9f2bdadc567a759b47273e7cd6521c003b9bf05d +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b829e64c7e6adfb3e536f2e49a6848ac7a57640 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 8 lines last edited by Alexander Raistrick in worldgen/assets/trees/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac8757b18025da709521532ecf50132d4fd459f5 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 300 lines last edited by Alejandro Newell in worldgen/assets/trees/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bade7cf689411121aa696fb2530998782147b17e +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 4 lines last edited by Lingjie Mei in worldgen/assets/trees/utils/geometrynodes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e2c20350f4dbfeae06714220494a6bd173345d71 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/utils/geometrynodes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 55b9c5ff3977effbc6dfe3d71c943d1af9ee15b6 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 57 lines last edited by Yiming Zuo in worldgen/assets/trees/utils/geometrynodes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f00b5fbc51fc93edb899ef9e4b3261a132b0c157 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 269 lines last edited by Alexander Raistrick in worldgen/assets/trees/utils/geometrynodes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c6df950470eab81b09b6a40981ef9cf16fcdaac +Author: Alejandro Newell +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 369 lines last edited by Alejandro Newell in worldgen/assets/trees/utils/geometrynodes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9fe6ae855715f4a5c3fd6e9e6d2c2ad9a5438774 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/assets/trees/utils/helper.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8ab8b5300b28de0bc0eed2c290222ee6ff62307b +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/utils/helper.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 703cb96178177bd16ebd4117ad943b41d293ee15 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 9 lines last edited by Yiming Zuo in worldgen/assets/trees/utils/helper.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9e53dc0e91a11050d1357485858f68cad2eae363 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 222 lines last edited by Alejandro Newell in worldgen/assets/trees/utils/helper.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 00bf0651331d6d483892c47ce3852b0fe668ba56 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/assets/trees/treeconfigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efd6b249c92b5fcb9501795afdc7b48a7abda688 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/treeconfigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d8bca28c0e9bbc9a6756dc2817ec12ad35aa6878 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 30 lines last edited by Alejandro Newell in worldgen/assets/trees/treeconfigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f6282d2febb6cf983470a9fc79aa37e718c4a04f +Author: Yiming Zuo +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 49 lines last edited by Yiming Zuo in worldgen/assets/trees/treeconfigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1441bd8bef9322c13fc483deb8bac29330d23924 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 689 lines last edited by Alexander Raistrick in worldgen/assets/trees/treeconfigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b6575781ec1ea500d58d378e2f932c6eb3b3c91d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e537899fb0d48064965f3963439e5972cfef418d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cf6941fdfccf8293c460ff2af43ba16eefe30f83 +Author: Yihan Wang +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75f5468cfb5a74290dea849fc62c8683644c5760 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 4 lines last edited by Lingjie Mei in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d3dced0f42f68e68a609cfa5d3b69b5b1ae81df8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 94181957782bd80918392d26c5dbf3e5d41e9755 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 93 lines last edited by Yiming Zuo in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cfa19eb7b517414d3f240a2cc0fb42c2aab5e951 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 305 lines last edited by Alexander Raistrick in worldgen/assets/trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5f328d5cd1c26fe5ac080d762c9c5bec2432a2a1 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/trees/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db42163d78ba6d4b49e2893a1ae4db045abcd59c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:25 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/trees/tree_flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2e2a05cb6ef675db56f929b9335212a788fa401e +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/trees/tree_flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 70c978a4085655831b520a824a30f7007c1b4f3a +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 9 lines last edited by Pvl-bot in worldgen/assets/trees/tree_flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ecd22645a2b51b853903c090a6709394c2fdd745 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 586 lines last edited by Yiming Zuo in worldgen/assets/trees/tree_flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0c81542b7656ee3ed5da98f2e3e5b6183f578d10 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/trees/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9cc8ff5c6732993bab9f35747d0f1382b6999541 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 38 lines last edited by Alejandro Newell in worldgen/assets/trees/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 11de6e2cd47d26896599acac7f340ec4cf660bc0 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 70 lines last edited by Yiming Zuo in worldgen/assets/trees/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 959e843b3577e3c6a952375587b98bb7773c61b2 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 90 lines last edited by Lingjie Mei in worldgen/assets/trees/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a7e92135279d9b826b8620c2aba5308319086e2c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 261 lines last edited by Alexander Raistrick in worldgen/assets/trees/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d06e44d82b896667c5902a84d44930a45ef1372 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf_maple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb2450e1f8c30e5adffc1b92cc8172627355529f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf_maple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f509eefc2fce39a5b5d8837c5c9b99b645060c42 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_maple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 83be410d3226955955b30ba9bc1e6a12f1f85750 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 785 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_maple.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b870767699992bd9d81505d48ad5627004ee4a2 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf_v2.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0b2800d95ac4294827fd17327a0a9edb4d5489e8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_v2.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 95d4b1a693c882ae5b7a4b0cedf9698cbe3e1f94 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 106 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf_v2.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4beb78c60849056129d351fa7bb5fc009e57ec38 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 889 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_v2.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e5f4333de1e97d260004150059aca3766d4389bc +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf_ginko.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7c62042a5f96113480b770786b722d30279de834 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf_ginko.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 07759e758ba7ff372ec5ff817d31ea9f149a50f8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_ginko.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9d54216f03feadf3bd2707b53669136c8b5d39f4 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 516 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_ginko.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2d42179ee86631ba3ef12620353d8ea00d327609 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf_pine.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 53775f547f8d5a7c125406e4051725480977f7fe +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf_pine.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c834eb8e19a35f9c4f0f4fbb3f9de36acc5136a7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_pine.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 80a50165775715263089ac58518b7bfc77e57250 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 365 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_pine.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 092ab6eae584f65323567aceb06a89ac50e4fdd4 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf_broadleaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c0a92750738a51bc6861ea6dae47e238de138092 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf_broadleaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 00f76c40917f5840d5894ef2076f0895e0742518 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_broadleaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8bdb8cef3b5a5a2cb8345d4e386a7bad8fb36ea2 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 752 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_broadleaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 063e727a43555a571372001889ef8acbbf8d76e1 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/leaves/leaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d8c6f642e800438ed389d7619ff97537ef058e24 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 169ceb117294cf67cc3c4ff2f84f63c347a11c61 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 7 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f844741042f4dcc906a52a1c4220300b4bba9972 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 28 lines last edited by Alejandro Newell in worldgen/assets/leaves/leaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d4965f724400334de747020b8eba9c059a1e1fa4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 112 lines last edited by Alexander Raistrick in worldgen/assets/leaves/leaf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6903ec714fd8fc031ceddfd218f540f97d699f7d +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/leaves/leaf_wrapped.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3becd13d6ccf35151c8da6f3f7478169c4ef89fa +Author: Yiming Zuo +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 178 lines last edited by Yiming Zuo in worldgen/assets/leaves/leaf_wrapped.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 17f7c957c63ab9b1393199d6b7b9050e3ea861cd +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/mushroom/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 727c35e9fc45532ee60121accbff61b312d260d2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:24 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mushroom/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 628ea3bea8d61c35915a07aefd8cee4d0810f2fa +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 57 lines last edited by Lingjie Mei in worldgen/assets/mushroom/growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b8a6558f206f603e1c9897e1f0c09a86ecf675b +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/assets/mushroom/stem.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 098f54bcc4cf25cf20a0762609413cb052880271 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mushroom/stem.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a6dfa53320e6d2734ecf45dd4ff4a225d894214b +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 119 lines last edited by Lingjie Mei in worldgen/assets/mushroom/stem.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b91266da784f58b88fa2792e4ba59816b1f89219 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/mushroom/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit aa9c7fbf0349dc19a975489ed2d0ab5b3e26e0bb +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/mushroom/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 189277bc8cb9c4da8575347d0a845b0fc90d40d3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mushroom/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit afd7e139c0af446b2d472e090516db68114c5d6f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 100 lines last edited by Lingjie Mei in worldgen/assets/mushroom/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2ca5fe2b541ab4a28e820f71aad347ee21d524e1 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/assets/mushroom/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b08d0e131808144826d233b8e25080758d9d714f +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/mushroom/cap.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed0aa4aa0848b973c645e8b09cf918f627ccdfa3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mushroom/cap.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ccf05b0a57018601f1f1984a0b7526ae317e13e8 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 374 lines last edited by Lingjie Mei in worldgen/assets/mushroom/cap.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5f046ad73147ac927a2386f9de26b4e565e3d992 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/small_plants/num_leaf_grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 93076e4582a389a4df0188b7bbe224082ae97e0b +Author: Lahav Lipson +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/small_plants/num_leaf_grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 65ffa2d1efe8159efe8bfdb7b2556e71971d435a +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/small_plants/num_leaf_grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 441fd40d6bc5b35de5edd973be22e7ccd46a00f8 +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 182 lines last edited by Beining Han in worldgen/assets/small_plants/num_leaf_grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit be8ff695511f66afd7ad9b5071a04393abf1f5d5 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/small_plants/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3f545fd6ed4da4b40f8d000097646942459b6c8b +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/small_plants/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2c1f5175e95531dbd7fe1bbc4ebd493e715d2878 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 19 lines last edited by Alexander Raistrick in worldgen/assets/small_plants/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e587d6806307089f5a828bb1d9ca3aeec516239 +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 734 lines last edited by Beining Han in worldgen/assets/small_plants/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 698475e2ddc6aa8cc52bcd5b55091c78e67c8132 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/small_plants/leaf_heart.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f9044e8758bccc5ad6fd4e81973f9fd8210d0b25 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/small_plants/leaf_heart.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 287afddaae4a857a72a78126f8afdcc99dc0862c +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 73 lines last edited by Beining Han in worldgen/assets/small_plants/leaf_heart.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 171a275e8618531b4f62d4d96811cf9a1027ad30 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/small_plants/leaf_general.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e01879e0f3cebff8a396ae9e592550d1dff8176 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/assets/small_plants/leaf_general.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9db4cdd150ac4b642e399084232c1a786341f512 +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 85 lines last edited by Beining Han in worldgen/assets/small_plants/leaf_general.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 20bc971e661c675e47af0d292d136902e40841c4 +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Beining Han in worldgen/assets/small_plants/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fbcc9c53a42049a5ec336770917283ef6266306e +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/small_plants/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit aa8b13fe9e267da51e0e6680c5cd9019c6900a63 +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Yihan Wang in worldgen/assets/small_plants/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb72caadd84a356163aa7510cd61fe5c5fd793e0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 13 lines last edited by Alexander Raistrick in worldgen/assets/small_plants/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6aa8c2daa272e46b49bf114b4bd258b41148cd71 +Author: Beining Han +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 505 lines last edited by Beining Han in worldgen/assets/small_plants/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7698c3bcc01c0bd7a48d33ef6afaf04bb810472a +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 00b915be7c9bd164e32fdc650307ff5d29512d5b +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 21 lines last edited by Lingjie Mei in worldgen/assets/cactus/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 87972fa868b4de5112d0c224247b4cfc13d340d7 +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/cactus/pricky_pear.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59a43b84cba35935fff59737603cb75b25b1ce7c +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/pricky_pear.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56a9f61b205b6f22c16816108f4909758f5dc491 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 62 lines last edited by Lingjie Mei in worldgen/assets/cactus/pricky_pear.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3819ebb818860da4e5f4a351ab56612a5e439c0e +Author: Yihan Wang +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/cactus/columnar.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 618ee22b2fdd02f4ae478d8ce8af3b5f15e9e694 +Author: Pvl-bot +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/columnar.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 03a5d4df8963eab55c93ae8f2de753ab1e11611c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 92 lines last edited by Lingjie Mei in worldgen/assets/cactus/columnar.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d72e5d11cd6534d6d5101400a51349b53754185b +Author: Zeyu Ma +Date: Fri Jun 30 03:11:23 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/cactus/spike.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2e2b237d8923f5643de7ac031dbc719f95a9c3f8 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/assets/cactus/spike.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0f3e353825c2c5f51f2396a8ebc3e2bb193878df +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/spike.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e6137b646c1920fafa2d11cc12de6b327a116cc0 +Author: Yihan Wang +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 8 lines last edited by Yihan Wang in worldgen/assets/cactus/spike.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 57b005ca95e0ecb762482c9768b32a9842c11464 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 98 lines last edited by Lingjie Mei in worldgen/assets/cactus/spike.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b0e7661bcf3dbc8d7a2328f29296d87eb2a42591 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/cactus/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 519981998e3fafe3d522f16b59e9f72037bde1b9 +Author: Yihan Wang +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/cactus/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4ef74e04f14ac44a9a5c54ca1e981eed3ab243f4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 490bb59675540dec37a91537c0b33cfd0107f074 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 89 lines last edited by Lingjie Mei in worldgen/assets/cactus/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 576967bfe4ce5c69173a0bbbea2e129f36b827c2 +Author: Yihan Wang +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/cactus/kalidium.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21d3051fca8a5abdabfcee47d0f2635255d600cb +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/cactus/kalidium.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 01e2463493ed24a452e3177dd32616b3356df030 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/kalidium.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c885f549a671ff6e621c5f43586dd0ea29303be0 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 88 lines last edited by Lingjie Mei in worldgen/assets/cactus/kalidium.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb0bf9868af187766d6a96caa3cd62435ad8e8d0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 54e39135e5eb2bd097eb7ac4cd6f955bd0481790 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Lingjie Mei in worldgen/assets/cactus/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d912c86ff8a55981ef98f3357dbf19838b20b4ef +Author: Yihan Wang +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/cactus/globular.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 971478d3c08cac97032be994469be298167cb192 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cactus/globular.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f3851726070fe284b6117cd35e7ddccfe0837d8c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 48 lines last edited by Lingjie Mei in worldgen/assets/cactus/globular.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 06dbbd2078ed71fc9dc67239819a0a0c76f0517d +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/assembled/dragonfly.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6181e03c79d4d87e33b5d7b4c4478afef091990f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 71 lines last edited by Alexander Raistrick in worldgen/assets/insects/assembled/dragonfly.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a1bdc3a0998c5dcfa55d3c65f2df49f87f4f9bb4 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 240 lines last edited by Yiming Zuo in worldgen/assets/insects/assembled/dragonfly.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 90dbe93030eb09933fdb53fc6493b56e18885060 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/head/dragonfly_head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 13bf7f13e7ba59f2cfe18b0dd153db30c126f84b +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 178 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/head/dragonfly_head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1058edcaebf78104df4d3809f19faf49a708db34 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/eye/dragonfly_eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0fdff51fee89d9bc61bfa25651aceb8ab2f50c2e +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 69 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/eye/dragonfly_eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ba1d44d79a1af333841b7775e3764e11f28949ef +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/tail/dragonfly_tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b37c8bf2dd0c362ef936d90ff32d24840f2cb0ce +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 378 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/tail/dragonfly_tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 483c6b3f3c2a689bed5d0f7a57278915f218c498 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/antenna/dragonfly_antenna.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f66b56f2f8233704e876e478fdb01126343f9d9d +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 28 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/antenna/dragonfly_antenna.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2c62e5defa7cb971950e4b4dc4b29f518961c802 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/leg/dragonfly_leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bc83b2525be9cd506c99eddc80a60044b07ade98 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 186 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/leg/dragonfly_leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 17229a7b0e38e5deb25137f477ec2c028e887b31 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/wing/dragonfly_wing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7e7589377c99c9250f0a7f3c654964445368a1ff +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 280 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/wing/dragonfly_wing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d270995586b5738b668270963ff31b1ed1a1f8fd +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/body/dragonfly_body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 142be2c07aa8801bd35e801ab3a5a58af5753875 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 225 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/body/dragonfly_body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 98f15744b153d2b936c50d0b6370cd4f0cb723a4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/mouth/dragonfly_mouth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c3b4ca49c47cfe21fca758d3612d472208e3acac +Author: Yiming Zuo +Date: Fri Jun 30 03:11:22 2023 -0400 + + Add 67 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/mouth/dragonfly_mouth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0e360496bea19f6068a0a7929b86c59878196d4b +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/parts/hair/principled_hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f06ed88664d00b92201f9926621287fdaae00030 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 31 lines last edited by Yiming Zuo in worldgen/assets/insects/parts/hair/principled_hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b421d7003cd24120b15b2d8c46af96e56ff6a14 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/utils/geom_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9adcc051fedf041a24373ab66afaa9149fe7e438 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 829 lines last edited by Yiming Zuo in worldgen/assets/insects/utils/geom_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3307333ebababe72e0c831c4e36bbebc95bfe0ef +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/insects/utils/shader_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d3f0e62ded603d45bf2fcc48bcb9817979b0eb25 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 87 lines last edited by Yiming Zuo in worldgen/assets/insects/utils/shader_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 72df0dac62a14e4ad3a2f4e3b540526df792d246 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/utils/object.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5401431fd877b8f3e9c1216cbb0bfe44dee4a6cc +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/object.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b5690c3ba983531e6edcf324c162e822e75ecdf +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 101 lines last edited by Lingjie Mei in worldgen/assets/utils/object.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1be69f22de9620d7c33c01057fbbea96959ca4d7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/decorate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2a593a19a53206d2fde2387fe7b8bd314c59f890 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/assets/utils/decorate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8c755190927477707ef818660590d37e9d77c169 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 239 lines last edited by Lingjie Mei in worldgen/assets/utils/decorate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 510f1e10c7140e01353c8a724807db2e6f3d62e4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/physics.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1710621e9e83011216e0904ee4ae15a41343b91 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 43 lines last edited by Lingjie Mei in worldgen/assets/utils/physics.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 20b4476deffa1453f08947f8f3a74f16e53cf49d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 3 lines last edited by Alexander Raistrick in worldgen/assets/utils/shortest_path.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4687f89070c567a43b09423b4b40f7fe3797073a +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/assets/utils/shortest_path.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 737b74b13f1c0b070a8091aa8c6b7edf86d4f9a6 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 35 lines last edited by Lingjie Mei in worldgen/assets/utils/shortest_path.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 060bea59a3b3675b45456a5b0f7ef1efc9ddc5e0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/assets/utils/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a528c48eff7b635b72b2306a9b8e0c0b25ba5188 +Author: Yihan Wang +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/assets/utils/tag.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d3f30fe79ad264829c4368e2e6c1f6e8dc80e8d2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/tag.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 57bc35f03c061a326ba41cae34d6d8d66895e386 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 139 lines last edited by Lahav Lipson in worldgen/assets/utils/tag.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1a534dee34dbcc79b8bd0b27b58599f45015493 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cad33f9ff0eae1fcca1b3a21b92a4ed95a066ed1 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 65 lines last edited by Lingjie Mei in worldgen/assets/utils/mesh.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8631a9751422a7591b2c67165b921d8e24bb4d17 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/diff_growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e7f7fe49711e0aaf6168f7ac8b0a82527dc10d79 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 81 lines last edited by Lingjie Mei in worldgen/assets/utils/diff_growth.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6065753ccfab7e5d78183315f414c96fa3c22a63 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/nodegroup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 38412a0a124ed44753ef3eacd3ecc8a649e89c05 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 84 lines last edited by Lingjie Mei in worldgen/assets/utils/nodegroup.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a701974073fee802fb517822098b6b9a3a98bc1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/draw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 65eec1620f00f97dc69f15de477ec0d68628bf59 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:21 2023 -0400 + + Add 155 lines last edited by Lingjie Mei in worldgen/assets/utils/draw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c32013a55f83661facb3facc468c665e737ceb17 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/laplacian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b14a1c568fb02c44b9a58a8bcedbc2fa40baac9e +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 96 lines last edited by Lingjie Mei in worldgen/assets/utils/laplacian.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 707f157d34e1c3c59f0721460e679bcff37b8986 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/reaction_diffusion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2a99ee22bf76c8dfbfc00e6e1aa9ef3199f84378 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 69 lines last edited by Lingjie Mei in worldgen/assets/utils/reaction_diffusion.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d344597523f46e00368723d8a7bd7a581ddd1ab +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/utils/misc.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit aced7028a1303a9316845c3e954ba5604ab7e547 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 52 lines last edited by Lingjie Mei in worldgen/assets/utils/misc.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff4ed5a64b00ee08d17a13a8a1be527f97c74557 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/deformed_trees/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d0538413c1ac05ce1d8a1a0c2e81a627a69eab1c +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f73ef6b01250c56799e602f9694d35a866b0adfb +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 54 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 05c4fad39bd00ea8032f13cd8232902c746be79c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/deformed_trees/hollow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 13a812e4224d4c6ce3841835ee5cfac3ee1a785e +Author: Yihan Wang +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/deformed_trees/hollow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eebca09139b393a3cb5298a8d79924c985028dd6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/hollow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85fb21d48326745d696bb386b2714b9491118277 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 68 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/hollow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c78d553503bfd20989ab5fb97d245284231f96df +Author: Yihan Wang +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/assets/deformed_trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c0b701697da307653db0d04f0d5acc18637683df +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f5478819e4f2d64af778fde85c74b9f2f141a237 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 18 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5838146649aaa36a618f12c55f12786659126725 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 5 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 937adca4fe3f79142315143889da4b84966e02d0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/deformed_trees/fallen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fc3961b0120d74039ef4f3458d73e37ec0945b79 +Author: Yihan Wang +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/deformed_trees/fallen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 04c0a997f324c0a4d3a1f5a02ec4bd9ea2b57e39 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/fallen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b54f153677ab77883c62e27b3b61ce02b7113715 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 76 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/fallen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 187bd18fe0537ed550ebe452cdb4c41239f78e53 +Author: Yihan Wang +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/deformed_trees/truncated.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit faad74edef4054fd62adf72c611ba2aea0e85d79 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/truncated.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d3d40dcce054193c20eb08bdb4b44e23c71e5610 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 35 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/truncated.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c991e0848f739c7875084c315806a9c97ac8df79 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/deformed_trees/rotten.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2cabd7f412274922aff7515000818ee85391221f +Author: Yihan Wang +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/deformed_trees/rotten.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed6cd70e7ef5666a5bb761d04fbb6f533b66b7c2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/deformed_trees/rotten.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5823a9e8ca1bc3cef7ae93e6f44ea8b67f75ea5a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 81 lines last edited by Lingjie Mei in worldgen/assets/deformed_trees/rotten.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit df779c3cce33bb74dd911a99b06395a5de07287a +Author: Pvl-bot +Date: Fri Jun 30 03:11:20 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/util/creature_parser.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bfe0436df208d4f89f76f7961c88ea6b82d3c90d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 135 lines last edited by Alexander Raistrick in worldgen/assets/creatures/util/creature_parser.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 18fce8601a024d39fcb6a29f3cd6b10c941ffc3c +Author: Karhan Kayan +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 4 lines last edited by Karhan Kayan in worldgen/assets/creatures/util/geonode_part.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e666c6814e592c698677a7c4d1831546efc656d8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/util/geonode_part.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ef45ed84dd6f79aedf1a5de7934a5f7002a187f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 98 lines last edited by Alexander Raistrick in worldgen/assets/creatures/util/geonode_part.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97646d353737cd34114a5fa32d48f8f3e7589bf6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/util/join_smoothing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 63b793d36d4e36c8d7e3756f434a362c3038d9c7 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 217 lines last edited by Alexander Raistrick in worldgen/assets/creatures/util/join_smoothing.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a71a6331ad69dc659075abf06f6fd4412ce63978 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/util/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e8fd58873deb24f7a8ed493ecb8a7eb471d7b1a3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 62 lines last edited by Alexander Raistrick in worldgen/assets/creatures/util/tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a22533f323ad7b53b308e6a621dcda6209d5f424 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/util/part_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c2b138966cbc46e3b870272f928e11547bcb01c3 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 43 lines last edited by Hongyu Wen in worldgen/assets/creatures/util/part_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7472ff532d6d6d42f4dfdf5b2014f4ca48b127c0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 169 lines last edited by Alexander Raistrick in worldgen/assets/creatures/util/part_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74c5e265676e620f94c440aca5a847d1a7d950f7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/antenna.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af1dfdf39d2c5297d2c474c7710f001d3ad1580a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 52 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/antenna.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6449159f7f35a479690faa870c7f7a83341b7d39 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8170a0134bde00f4fb0e4a34dc766009e425f59b +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 29 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 67a12ce06e6c4314526718660deebf6fbb3d61eb +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5ad9f97f11f4a689311abe74fee1a34cd2252723 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 78 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8735b715042fbd973075d7992eaf2828ce88faf4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb9350f3e12111a0e8a3de5ddf20c42b10c8179c +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 33 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0b1476aead15787ab55e79bde8e93d59bb4f280f +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40304e10c3b4eb7b90a1271dd55af4ae1c5651cf +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 82 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a24a50efa21a0af1d9f245dd5d7f6948328e6c32 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85f58288e621af812bcdbcdd58b8df3f45a68e64 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 271 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ba8722f71ed0a521c967547c07ed8583a68c826 +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/crustacean/claw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 34955ea00f3ba0d5b1581b8f2501892eb24a2354 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 166 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/crustacean/claw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 79b45f9c23991ff5a3dd6bacf157ca172940a60c +Author: Pvl-bot +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/utils/draw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4c5f4fb376b7b96e008036c8c25ee8100b2b03ba +Author: Lingjie Mei +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 56 lines last edited by Lingjie Mei in worldgen/assets/creatures/parts/utils/draw.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9672da989fb1ce89718d2ff86f69880ebf0dd809 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:19 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/fin_old.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d8ba795f7406232ad0126686c4d7765d6f5c299b +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/fin_old.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 983e237798bdab9702b484636de3a2a58fdfd62e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 105 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/fin_old.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 49ebf25061994944110dfbda08ecb12045445dcd +Author: Yihan Wang +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/creatures/parts/ridged_fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit faa6619b19bf3afa6cfaeb6c08fb091587d72f59 +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/ridged_fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b7ff89d5a57bb283ab911957f9a97d561a7ae86e +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 193 lines last edited by Mingzhe Wang in worldgen/assets/creatures/parts/ridged_fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bbff5f927415b8029c3fbdfbae6f2c755ceb7c1f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 275 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/ridged_fin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7c286ca38486722766cab9e2fbb34d6621693161 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/foot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 77cf4868bf846df45e26e8d7b6888070bec39a13 +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/foot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b9b91a5108366c095090c4762c906bd27168580 +Author: Beining Han +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 127 lines last edited by Beining Han in worldgen/assets/creatures/parts/foot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 34d6fa5de7a137632e9bdd06b7b902cee576a4cf +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 159 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/foot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b60d2d6ec7e16b4dd6468f451f8663140d0a278b +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/eye_new.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b21294f55c3060253984063da01384e390cda93 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 2425 lines last edited by Mingzhe Wang in worldgen/assets/creatures/parts/eye_new.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 96f78c135b08bf1f158abcce8613a3ab0d9dbd1f +Author: Karhan Kayan +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 2 lines last edited by Karhan Kayan in worldgen/assets/creatures/parts/head_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fc10e38b3d9dfc74c696dc475bb7b45eb7153d4d +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/head_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cce5f0e7f2d055c652bd0723a1e078cc2a75df72 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/head_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 724a60d242b0dc1e271c51bcc1eb38d142e0a425 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 189 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/head_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit accf12cbadd0332f1cedde7a5e58d3b762609f02 +Author: Pvl-bot +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 22a9ad6181148caf18e511a83206713161af5ec0 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 6 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 41f90d29b964741008240dbc787b3f4206a9d722 +Author: Beining Han +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 14 lines last edited by Beining Han in worldgen/assets/creatures/parts/head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f6395e08f8bdc07c517583a94ff1959a74589227 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 597 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/head.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21e885144f94872f30ddbcae47231c087cd554d6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:18 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b15bc841f202f4f9309b77c3f2e873a8c9492a7f +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5490afa859ed5a4a93f43a5823e001c858cf7e4a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 263 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/leg.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d65e47e8e06e26489850acfbd6961ca9a78ef1f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/wings.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a01c9f0c208ea6c41498b07eba259bce06f3b85 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/wings.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b4943cb5e7ecab70388fe866597f239d79b6a18a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 301 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/wings.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1847dd9ca523d267493f87da2a6c38002b0d9ce8 +Author: Beining Han +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 359 lines last edited by Beining Han in worldgen/assets/creatures/parts/wings.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b4e3f77200e1327b3172f5c4a090b97616c9239f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af1f2543a1e97bf31bb5790ef4a2fe807fbb0224 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9283f2d36414c5a94211b92b920230102d0ecbfa +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Mingzhe Wang in worldgen/assets/creatures/parts/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f651188d061f06b386db5dbf92be5b765a9886d9 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 153 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/eye.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3280342de4602422f00095f857647c8711f97a38 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2a390f83c026e2eb6f40b1decc5b26e79ae17538 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a01ae86f567a34b7243a4f841adff26e039f398e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 47 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/tail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 780beca5b505ac70a52fa10f2aa0e1054d847fb5 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 1 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 49e28a4d95a4ffc7cb79c472d7a3a2675fae575e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2173db844a8ff4bb082221c4223b07a79a5c35e0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/chameleon.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 01e97ac0313d1c501c4dc2d914ac290da503aab3 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 1592 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/chameleon.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d4456b504acf69e9c2903a69b0206ca21a77e5d5 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/hoof.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 184ddca5d357d37ce9e934820d27fc947dd2a095 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/hoof.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac45fbc25665ea2f4d0e454fc490a38e07bda557 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 21 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/hoof.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4fb3b14cedcd691836bdcacb8be0077744db375e +Author: Hongyu Wen +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 177 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/hoof.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ab74d593a0d9d9f3b3bbfa81538e2c9f50cd0b1c +Author: Lahav Lipson +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 23ffb961a957cb4d9dd8cedc72ae9bc754df02f8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 220abbcc91f97cc14c112d4e54eb3bcb96f50114 +Author: Beining Han +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 14 lines last edited by Beining Han in worldgen/assets/creatures/parts/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5e94c9368bf736a84d7754dc5fdd7f781891381b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:17 2023 -0400 + + Add 199 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/body.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 71e19b570e967b549a93195dd3c7bf8a6539a45a +Author: Hongyu Wen +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 368 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/beak.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1c34beb283aff36576821650a83b1a282d93ae5 +Author: Yihan Wang +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/creatures/parts/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f82afae24e139b74b62f20691b468ea8188b542f +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f692b4d67d65e46d8c4027441bb2aca9e40ab63b +Author: Hongyu Wen +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 90 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8783dc34b56a8a5584d5091ac0a1b53f63d9e4bb +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 175 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 814803614f4507fd480b6ddd83969a6c08a50530 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/reptile_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb64ca465a7b6eff0d9853dc66f0bc7582a18780 +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/reptile_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59f6bbf54e5e9fb2e60d2b6e5a55045a8558c8ef +Author: Hongyu Wen +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 1386 lines last edited by Hongyu Wen in worldgen/assets/creatures/parts/reptile_detail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 27456f3c02880d1471a5a67097e8ddbd74e40232 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/assets/creatures/parts/generic_nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d803963f6925a1df879187f5f86a9e00cb6e27e3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/parts/generic_nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2f56825bf85cbe3187d00cc73cb37a9d8ad8d2d9 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 155 lines last edited by Alexander Raistrick in worldgen/assets/creatures/parts/generic_nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 990f031486170b964981bce11db5e362a4420a0c +Author: Lahav Lipson +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/assets/creatures/genomes/carnivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5ce5e20c404a37536f19d4317f24dcbea18f6b93 +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/carnivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e350c0b1c1d21796c0ca9582d07a14f3e5173120 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 214 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/carnivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0a96bf67949087bbe45f29fcea2387af4d6849f8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/reptile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9544466aed78499a38fbb86f07b92e2147365a04 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 126 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/reptile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 266ef52c97ed1b4270325ddc00fcf3a09d39a0a2 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 410 lines last edited by Hongyu Wen in worldgen/assets/creatures/genomes/reptile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 69dd9eaf7b2b2bdfb3748f747c5e8822a7633cc1 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/creatures/genomes/herbivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e3f0e29ece4835464bc5e4c512a3c57d4e68701b +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/herbivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ba271b8b06376f45b828853b18a09ddf84507cf5 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 8 lines last edited by Hongyu Wen in worldgen/assets/creatures/genomes/herbivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0f2178e5f0e654f08baeb935bf2a3dcc7eaa8e7c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 219 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/herbivore.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a778e8aeac7ab38bf95eaebd85b3a9abd869c092 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/assets/creatures/genomes/beetle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f6133be082f9d057e67f0f4ab4fcfa02316e19b5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/beetle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c2922899ae6fb48e5d709bf9c455a0dc96d4500 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 193 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/beetle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4e5f7959236bc96435ffe75c5ffef9f764c91559 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:16 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9ab62f63fe519cd51c2ae1ca302bbb3cf16e925b +Author: Hongyu Wen +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 2 lines last edited by Hongyu Wen in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 160e451fbf43423d452f9c67ea2771f2c543d38c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 4 lines last edited by Zeyu Ma in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c210e18356612770a4f193fab0724bbd2291f0e4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 92dcb39ab83f6017a1e1a501f014b11d271fbd3a +Author: Beining Han +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 100 lines last edited by Beining Han in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 30cefb8c88c4d208ad066a96c356309d064ec4bf +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 221 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75fd21564a589447c43035b838130a6fa00c852e +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/fish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0d582c14803cdb87a60f1845dd6cd19c5256cf8 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 31 lines last edited by Mingzhe Wang in worldgen/assets/creatures/genomes/fish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 36d58f373d3256a3fd4b2004f63664b553a0933d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 289 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/fish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 69fb6d85e4c2c333e45c26140f824300d4b27d07 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 4 lines last edited by Zeyu Ma in worldgen/assets/creatures/genomes/crustacean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bbe1fec3619ee7462162c0857d88fc0242bfad51 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/genomes/crustacean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a0f510fb557acfe471866895485d1ae059b17861 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 7 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genomes/crustacean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e944b5177b9a1a28f337922c9c43b3b5ce14296 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 302 lines last edited by Lingjie Mei in worldgen/assets/creatures/genomes/crustacean.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 77ff1a7324ea33ad68069f109a47378650b5f354 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/tools/dev_script_save_nurbs_handles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 232f96dc30719dde69a5e0959c1bd1d834210c25 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 37 lines last edited by Alexander Raistrick in worldgen/assets/creatures/tools/dev_script_save_nurbs_handles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 777eef102df53be31e7aeafce685ca20c15048a0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/animation/driver_wiggle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0ee1defacf98977bed1d5dc0d99ce5846c7ac804 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 51 lines last edited by Alexander Raistrick in worldgen/assets/creatures/animation/driver_wiggle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb961960e4dea162e324483d8caabdfbd5da9d7f +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/animation/curve_slither.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 116797cd802970951bdce3a5f3ca0dd62adcde6d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 208 lines last edited by Alexander Raistrick in worldgen/assets/creatures/animation/curve_slither.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f82f3dce8834597deadbfaf2ac64984767326df2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/animation/idle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2119a9bfff11dcff2401b914d4c641b5d77215e0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 147 lines last edited by Alexander Raistrick in worldgen/assets/creatures/animation/idle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85acc7cab889620bdbf267c9310cd99bdd3605db +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/animation/run_cycle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 819d8249e40e0daac502a0665400edde593bad3a +Author: Hongyu Wen +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 142 lines last edited by Hongyu Wen in worldgen/assets/creatures/animation/run_cycle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 337fa9b0679f3f1ba984779642f1e52ce1dfccf2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 185 lines last edited by Alexander Raistrick in worldgen/assets/creatures/animation/run_cycle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7cc5299abf9d9e23f501dce3d06382e641af8a2f +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/animation/driver_repeated.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 581b3570e3ae34d7f8e10a5bfb92c751b09662a7 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 41 lines last edited by Lingjie Mei in worldgen/assets/creatures/animation/driver_repeated.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cae6979bd8a30edda68702745d8aa97b1a9d9163 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7015e0fe8eada1ca631ca8a18bc1dffa6be3d8a0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 197 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/math.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit afbdf4fad5af6952ad7d1ef1ae126fb5f149a517 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/curve.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 18660518145487759a126605d34f361e67b2e11b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 364 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/curve.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 388590e7445d3b0e5df176f4e96f4378a3bee7ed +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/geometry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bf5f977b989b51f8ab1067cd6bb501747c027daa +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 131 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/geometry.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 568cfb7f988abe73c033b8cc09884a5260eff572 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/attach.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 703c735d941edcb4fc2490cf2240bcc51aab95d9 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 228 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/attach.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 871f3aacf36c45fc86892a36f1c96df0ad15a771 +Author: Pvl-bot +Date: Fri Jun 30 03:11:15 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/shader.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a032cb3f81478f0090d466506dff6fdc1b3a9ed4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 180 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/shader.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e6b76009f64e8d8fc2a4e3acc13cbb744ad912a1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e9b0436aea4e60bcefa5ff1f9862da5316172c78 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 288 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e0ba75a6943c7df0a362dec5213c7ffc8456a2c1 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8af483da9847d95fcead545a1319bca7dbc5e76c +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/nodegroups/sculpt_v1.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 368cc8949cce9057a2129f6d1153b08ef46b4e9e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 245 lines last edited by Alexander Raistrick in worldgen/assets/creatures/nodegroups/sculpt_v1.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 44bc141c1de1838487ded6e0e8cca3322908afee +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 22 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/cpp_utils/setup_macos_as.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a7b4fb76b3038c69cb8e0df68bb14c610d04aa4 +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 21 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/cpp_utils/setup_linux.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 557539bdf82442a6faac87033b9a38213ea46b5d +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 23 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/cpp_utils/setup_macos.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5914c4f00fcae496a63610ca566bf6004891796a +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 160 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/cpp_utils/bnurbs.pyx + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e537184f424b9d01c2a951430dfbd45de48a16bf +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/skin_ops.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59ed8729a1d8648ec48e7c73f4e6186e7f33dffa +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 23 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/skin_ops.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6e3cbde52f70c2b2558308aadd2fd9b47077fb06 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 94 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/skin_ops.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 169f01789a7dbd1c4c20077c1af494aa26b2d069 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/metaballs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 301adc8425a2f0746b149686ebfc1e950757067b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 158 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/metaballs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efc371d8981c7f6b015a4400f510445ef0c24e97 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/curve.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2c026530b525138045304ab987b96c5d1f46ab9c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 92 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/curve.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d5c741da7a58a901898d5fd54061bc58b74560f8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e2eb3ba64054190a075a81ea06e84aa6e6393794 +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 150 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c55cb40494796974f70849f7e3d85ebed94d2982 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 169 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/nurbs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2268b4a61e91a17172ad9b1c6b77baa869b29346 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/blending.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 16c915daf17cea85ff23f184d6b0dd1a85a06628 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/blending.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f2fd7d327ec6cb8404f6e38c0be09389c9af83fe +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 1164 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/blending.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 331f7f1834bc88764414391fc99fce1d91a9f834 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/geometry/lofting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit afd348d17c155169c762a60fd55ceb02fc3717db +Author: Jia Deng +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 15 lines last edited by Jia Deng in worldgen/assets/creatures/geometry/lofting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9caa4ff3a44f4ca47e0e3e77fadb24cc47446959 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 103 lines last edited by Alexander Raistrick in worldgen/assets/creatures/geometry/lofting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 044a12bed86558a18fa4d9ee12ef535266ce4053 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/boid_swarm.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e8e58ed6a1dd8e19eb9e721cb6d20bef0bf516f3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 92 lines last edited by Alexander Raistrick in worldgen/assets/creatures/boid_swarm.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac52cfc3d11c0227d8377a41a6f0d675df8a192f +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/creature_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 06ff1028f196d5f20912834e70fc93659b5e70f2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 94 lines last edited by Alexander Raistrick in worldgen/assets/creatures/creature_util.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb5d704fe9265a15602eb3a0deb8b491c338c136 +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/cloth_sim.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85cb3d77ffc877d2937d540a4324c7f3e9f04109 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 115 lines last edited by Alexander Raistrick in worldgen/assets/creatures/cloth_sim.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 082c42b987bc579c9cf01cd5f1cf9dde1536f19b +Author: Pvl-bot +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ec7d68d58aa3ad5dc456e3366e9cd46c307bf147 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:14 2023 -0400 + + Add 209 lines last edited by Alexander Raistrick in worldgen/assets/creatures/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 161dfb5339f456e76d2c5f77e6ba99f30a689539 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/creature.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 404284b843aba05611447a667bd56da14f96e092 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 48 lines last edited by Lingjie Mei in worldgen/assets/creatures/creature.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d0be183ff0467d069df20e5bb0a1c7c8f05fe13 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 267 lines last edited by Alexander Raistrick in worldgen/assets/creatures/creature.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 690d466826debc49846b135b9afa7fad95e68a89 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/rigging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4f6a3ac595d2ce55bdf68a7768c357d8cf38cab5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 51 lines last edited by Lingjie Mei in worldgen/assets/creatures/rigging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ea7ed03dcc92829a1f030e93624c6b885f1de288 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 372 lines last edited by Alexander Raistrick in worldgen/assets/creatures/rigging.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b72bdefabdcfe7dedb5afdbea04af0e3790abb2a +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9c7a09150584b31213ebd88dc04d917faac986c5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 389 lines last edited by Alexander Raistrick in worldgen/assets/creatures/hair.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3a62209640e5ffb70794b9d080ed7c34cb78fe1b +Author: Beining Han +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 1 lines last edited by Beining Han in worldgen/assets/creatures/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c72d20085c5ab94acd05c0fec6f2918d5bcd105 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/assets/creatures/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c7a681239b1c156c40a1f89d9ddf7aa2665a55d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/assets/creatures/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 09db66bc87a9b38570e7379e8e578b9d46c610c2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/creatures/genome.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 39a23ea9cce9dfae0517a3345fef2583c253e1b0 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 36 lines last edited by Lingjie Mei in worldgen/assets/creatures/genome.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 36ff7176ad82237bc93897cc56bb24cefecc5815 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 145 lines last edited by Alexander Raistrick in worldgen/assets/creatures/genome.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 978defa0e5dce600a062f6d93aa702829271b837 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c380df557175b214b7d9a523fec6f7c07e7e1194 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 2 lines last edited by Lahav Lipson in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 55d8921640ae8844518ea57c9ac7ec2ec34681c2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e40f58b92f7ee40ff130ad2b3f31712d604bb228 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 286858019f00028b40dd4ffb1bdad0d8f3ae0da0 +Author: Hei Law +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 45 lines last edited by Hei Law in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 50d50854e64f75c233000b39bdc7dd0d19b96e13 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 84 lines last edited by Lingjie Mei in worldgen/assets/cloud/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b2e6ce898dad23c5bcf10067bc59044dde30344e +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/assets/cloud/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 834b615b65e67d87105418fb4affcf2b4aa7e11d +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cloud/cloud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75a46cc286ea4ef145c3ae804c5788cc6c960c6d +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 11 lines last edited by Alexander Raistrick in worldgen/assets/cloud/cloud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9bb7184098039091520953c52efeb5b1b3480b17 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 124 lines last edited by Lingjie Mei in worldgen/assets/cloud/cloud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74f4a8d8044dba507002e204f973f45a37306d0f +Author: Hei Law +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 422 lines last edited by Hei Law in worldgen/assets/cloud/cloud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56cb68aee9dd7348eef1ce2f0c262e6359db6034 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/cloud/node.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5fdcee6e9c0a6dfeb9f3ea6988724cde84d8a46f +Author: Hei Law +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 805 lines last edited by Hei Law in worldgen/assets/cloud/node.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9ceab46f4271a471f9934cc12cda72abf2a5923e +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/tropic_plant_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cfe8bc57976ca0417bb3ece5b6f1c5a891ce48c0 +Author: Beining Han +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 821 lines last edited by Beining Han in worldgen/assets/tropic_plants/tropic_plant_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 23d148e270c7fb0ca6414fed1d1dd5ba36251f70 +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 04a85ee681d09be8d6fa73283962d629212df43c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 7 lines last edited by Zeyu Ma in worldgen/assets/tropic_plants/palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2a3edef8ddffb42862ba6a5682eb69d3ba8549bd +Author: Beining Han +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 980 lines last edited by Beining Han in worldgen/assets/tropic_plants/palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bab60719f1add3290c20b7697c8ad491e9406d6a +Author: Pvl-bot +Date: Fri Jun 30 03:11:13 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/coconut_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 32b39c8dc8a44464eaf3c5980bac3d1a17424dcd +Author: Beining Han +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 1288 lines last edited by Beining Han in worldgen/assets/tropic_plants/coconut_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0f3713921e013ccc7267a71ce5cc75102e149dcf +Author: Beining Han +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 5 lines last edited by Beining Han in worldgen/assets/tropic_plants/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b3181b2d715416dcd3e5fb49efebcd5fe73fa0c7 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/assets/tropic_plants/leaf_palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 64a135f766b4a4cd1388a78d0c8e9bda5827fa1f +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/tropic_plants/leaf_palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 49b6b485ff450f1ca41c6d5ba87d41deb43913ef +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/leaf_palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c01224a7896e2758beb5e3fe90b19045546735fd +Author: Beining Han +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 625 lines last edited by Beining Han in worldgen/assets/tropic_plants/leaf_palm_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 89417899e8b97928c94fed27025441cd1610374d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/tropic_plants/leaf_palm_plant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 05c0c7556950f5019c4012c6733df98452a6fe2b +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/leaf_palm_plant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97802868a06df24a169da14ff98cdc94be0a46d5 +Author: Beining Han +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 545 lines last edited by Beining Han in worldgen/assets/tropic_plants/leaf_palm_plant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a7fe96eda0bb3f74d1c16351a5887782e3bd91b6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in worldgen/assets/tropic_plants/leaf_banana_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 401eb16d7d62599864690b5b974c998294a7a370 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/tropic_plants/leaf_banana_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d11516020d320d64feb7d946b8577f280a5f8b4d +Author: Beining Han +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 666 lines last edited by Beining Han in worldgen/assets/tropic_plants/leaf_banana_tree.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cf0418549daac9a88fd60af8f1a8240de1d25301 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mollusk/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f15a2aa0a1f9300572fdf2e4296f41362921a15d +Author: Lingjie Mei +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 18 lines last edited by Lingjie Mei in worldgen/assets/mollusk/base.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8a777baea85f658586f1926d67e1e7b3a837108f +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mollusk/snail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1843e4a7f529b683fedd868a4d246654e254671a +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 15 lines last edited by Yihan Wang in worldgen/assets/mollusk/snail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 54b408ab5cc98e1f269a3c4c67af50fc9e378d1b +Author: Lingjie Mei +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 147 lines last edited by Lingjie Mei in worldgen/assets/mollusk/snail.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fb20f0c5970d4e61b05ae15c5257a5ac8d97fe57 +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/mollusk/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9accb8dc8a14bd9d72e064dc67f21fb8f494a7b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mollusk/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56259dda65db0ab27e51908508de3ea356fdf139 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 126 lines last edited by Lingjie Mei in worldgen/assets/mollusk/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 28a8571a9c7d267d611f5891dd256650b1cb69f0 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 4 lines last edited by Lingjie Mei in worldgen/assets/mollusk/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74e6d9df883bd5d1ef2420006788fe35b50aee29 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mollusk/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 04c68164077caff196c34cc6b5e259351fcc42bc +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/assets/mollusk/shell.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d2812ef948ac6f675cfae47a9bbf70d473d6f582 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/mollusk/shell.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 27277176c84588c86a9a4071cd533642d9bef483 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 150 lines last edited by Lingjie Mei in worldgen/assets/mollusk/shell.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a207c09286adcaba04cef9457a085d31bfa3ae69 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6676198b95a3411843f67d61684931662a520964 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/assets/flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0a5a7e3a915bbfebbd94edbbfda39c97ef6cd725 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 46 lines last edited by Alejandro Newell in worldgen/assets/flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cdf57e70cf310d187cb5c351365b3b55e62c00b4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 540 lines last edited by Alexander Raistrick in worldgen/assets/flower.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9241d064f307aff8f96a30e33da4d0f1c00acd38 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/blender_rock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 15d024aaa0b25d61fcb48f6d9ddd28265a1a44be +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/blender_rock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b417fb9737dfa74b2625d0892fee3344b0ce8700 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/blender_rock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 401373d7c6c62a0d4a8750446b4cae76318c0adf +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 46 lines last edited by Alexander Raistrick in worldgen/assets/blender_rock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 60d4aead9f4c43b7a5edf610603a5346c7a11667 +Author: Yihan Wang +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/assets/boulder.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e4bb6f33c99ee020911bb29319d89c3fc5da03b3 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/assets/boulder.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 897d18d1fbfb2c61b0ebc36fc86c2500937c1676 +Author: Pvl-bot +Date: Fri Jun 30 03:11:12 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/assets/boulder.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 490287b3dc833205fdfe02f80be6a3d43f554b95 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 64 lines last edited by Alexander Raistrick in worldgen/assets/boulder.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit da820c53201c1acc6b19fde70c76c85c25f8f9c3 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 70 lines last edited by Lingjie Mei in worldgen/assets/boulder.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 238be4f5fd2053321f71e9860d2d1ac831d985e3 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f4c74d360c32ac66326e22356b6eabdcb49f8b88 +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/assets/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f41ea6ee3a3d6c5b6fff911b93f51abcae6056ca +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c52291c9c7c91412b63bca4e5b6869918857e053 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 147 lines last edited by Alexander Raistrick in worldgen/assets/particles.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d65ba87682d13e1daba081a2c9a388ce0b790bd6 +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 3 lines last edited by Yihan Wang in worldgen/assets/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dbada6344b61f599f3e36d23189405c7ffa057b2 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Mingzhe Wang in worldgen/assets/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 954e537235103dd240707214be799b3c888a3604 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/assets/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 50e0dc44cd5af8cfa8e32ab45280fec21b10f3ba +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/assets/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3c53977c2232d9e3041206ad6f8c9226b1042a40 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 246 lines last edited by Lingjie Mei in worldgen/assets/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 30832a3202a97d943aaf9ddf2415bbaeb43bb47c +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/glowing_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c0fbccfbcfb74ef8f8296daa2aff283337dca999 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/assets/glowing_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ae6c94f23b6348624539480b807c1ee2b4e2040 +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/glowing_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 146592dcf44658985dc43494ffbc1bd726ca5959 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 7 lines last edited by Lingjie Mei in worldgen/assets/glowing_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 65b51de615a5f001e70d1e68820eba0fa15ac571 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 56 lines last edited by Alexander Raistrick in worldgen/assets/glowing_rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 453c764c173af77979ccb2ed4a4d5ae7b1d26d9e +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 86718b778acfbd1576a7d8d85f08bded5b44cc6c +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/assets/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7e11adec31eeea003c207d9093a8743922e1b83 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 119 lines last edited by Lingjie Mei in worldgen/assets/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d37ebdd331e49557a675b6e30e032009ea7746dd +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ad739f2824fd16266e63215b43c880751aa8515 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d19a71c9019590d6dc7e5c3c8f60624b3500d242 +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b6b15bacd18b735424b0e91bab698e31ac6e48ee +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 116 lines last edited by Lingjie Mei in worldgen/assets/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1921142675561436c0b11fdb8bacb0f9c292f4e5 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/assets/pile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a70265d34d50a16f3c6d1e055bfd4788bf3c38aa +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/assets/pile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4ece2cddfc0c7392655ceb26d0088cddbfe885e4 +Author: Yihan Wang +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/assets/pile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 969bc1f4ed6d983fcea44fe963f2cc48ab35d49a +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/pile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 373d0942ef1cbeb948d0da8a0db2ecaf9ca4d040 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 86 lines last edited by Lingjie Mei in worldgen/assets/pile.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit caef62cdf63b01ed56e8ab4d07b3babedbb7a146 +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/assets/caustics_lamp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a635d1774a442d3e14895914e790d796325a42f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 58 lines last edited by Alexander Raistrick in worldgen/assets/caustics_lamp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 504aa4e0e5bb59ee526852ee8d652732a1792cd4 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 60 lines last edited by Lingjie Mei in worldgen/assets/caustics_lamp.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c48977d817594af5ad1525bfa8ea478f733969f4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 165 lines last edited by Pvl-bot in worldgen/lighting/kole_clouds.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b95dd1c0a66cc9377308be6b94d49f8786d5b1ca +Author: Hei Law +Date: Fri Jun 30 03:11:11 2023 -0400 + + Add 1 lines last edited by Hei Law in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 45fedf8d83b90e5fc38bf8fe8ec70df7c010ee2f +Author: Kaiyu Yang +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 2 lines last edited by Kaiyu Yang in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3db7f17af3cddc3745d2f417e610bd9e06dcc3b2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a165c6de82c21ad0973d3b1768d18a3114cafc47 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 11 lines last edited by Lingjie Mei in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9c39b8311238cc1a795699fb454f948cc0507440 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 23 lines last edited by Alexander Raistrick in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 877a1e87f5bc97c32c9e9ec5ed8cc71a68548e00 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 38 lines last edited by Zeyu Ma in worldgen/lighting/lighting.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c95c814dc9c55945f0265955a9c8f6725b556256 +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/nodes/nodegroups/transfer_attributes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b358c623b1946b7faa20d66987bb7089ad36974 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 15 lines last edited by Yiming Zuo in worldgen/nodes/nodegroups/transfer_attributes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8352d7a7f803f22205f565fe32a8914673f36e81 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 24 lines last edited by Lingjie Mei in worldgen/nodes/nodegroups/transfer_attributes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8c4b1ee16913d56bff4ff63d9a900acd2213b276 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 66 lines last edited by Alexander Raistrick in worldgen/nodes/nodegroups/transfer_attributes.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 94810cbea5e6cd65aaab1fcc7c8c10352e4641ed +Author: Hei Law +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 1 lines last edited by Hei Law in worldgen/nodes/node_transpiler/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2b313bb5b21e920f56e56d18fcf8fdcb806ab00f +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/nodes/node_transpiler/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8fb40f4a83581826bc0cb292d598d5ca39dbb210 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 70 lines last edited by Alexander Raistrick in worldgen/nodes/node_transpiler/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1f459be67af302484d91a9e52ea667ec457e1262 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 16 lines last edited by Alexander Raistrick in worldgen/nodes/node_transpiler/changelog.md + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac19dc2c6a6bfe779014df26b58d422e39ee777b +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/nodes/node_transpiler/transpiler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 696cbda1f206fb33b78178dd687723212992a545 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 16 lines last edited by Lingjie Mei in worldgen/nodes/node_transpiler/transpiler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c61f21920767262cb7c7547b1b6479a26327e191 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 28 lines last edited by Alejandro Newell in worldgen/nodes/node_transpiler/transpiler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c24f705e25fab67bc1a2e50f5b5e2be456f6f71e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 680 lines last edited by Alexander Raistrick in worldgen/nodes/node_transpiler/transpiler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 284a5f9eaa94e4f85f40e83291dcdb4955b363d9 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 1 lines last edited by Yiming Zuo in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ac5f29f40e491101f5e479061b5e6ab9a0103eb9 +Author: Alejandro Newell +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 5 lines last edited by Alejandro Newell in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a1ff763f9650bdeb1c0452e26fea4be8dedc3f56 +Author: Jia Deng +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 5 lines last edited by Jia Deng in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 628cbe7df33b1871db4abb44b65404add0ac37e5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 10 lines last edited by Pvl-bot in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a42f50b78412a463e7c405a98fa31d7b82945f66 +Author: Karhan Kayan +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 18 lines last edited by Karhan Kayan in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fcfaf14b59a48c88dc180c59aaf6f0e4aad3f11b +Author: Lahav Lipson +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 20 lines last edited by Lahav Lipson in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d37161315d63157c0f8c3214894a0efa64c6e831 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 32 lines last edited by Zeyu Ma in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 91928425db2b4cc9ebd80fdc875adb580c7cffce +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 199 lines last edited by Lingjie Mei in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c78f3b27bf09f5b3724339dcab812135a1dea794 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 245 lines last edited by Alexander Raistrick in worldgen/nodes/node_wrangler.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c4220eb794dc7a410c389101755ab09d5e72f1b +Author: Yihan Wang +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a6bc2e273a022605c2176b4e717c01d213b5e19a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 4 lines last edited by Lingjie Mei in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 228e72df12fa7faaae2af8adb569b9cedc61cd13 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 5 lines last edited by Zeyu Ma in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6be930ca73c798b818a8e9de85814e24bfb9cbc7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0b45e1cf22e75428f91b3d0be371c0904f7f0aa +Author: Lahav Lipson +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 10 lines last edited by Lahav Lipson in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 72f85d48b7862b5b7a47615efce0e1ab3d764fe3 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 23 lines last edited by Yiming Zuo in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 576c3bc5bf7e854ed85876e034aa63a982efaf13 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 104 lines last edited by Alexander Raistrick in worldgen/nodes/color.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 92e45a535cdc7ae24ed43fc3fb99b97b464930ad +Author: Pvl-bot +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/nodes/node_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 862dec69837a310bffb673e1fdcf9bbcff0f0a2d +Author: Lingjie Mei +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 22 lines last edited by Lingjie Mei in worldgen/nodes/node_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cdc0aafd27530257a23fe54059fa127a98b588cb +Author: Lahav Lipson +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 23 lines last edited by Lahav Lipson in worldgen/nodes/node_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 33454fcaaee8a4baf6e1c0c0a2a208e45b00826c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 54 lines last edited by Alexander Raistrick in worldgen/nodes/node_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a7f2e94b9f011e0a0d4879e20974d569de47d26c +Author: Hongyu Wen +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 2 lines last edited by Hongyu Wen in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3156f4bf088704d2799a14d9aeb62d47c9907c68 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6b068b507f37d07df870f7d1f84140067ca7d684 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 4 lines last edited by Mingzhe Wang in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dbf09aafdd3981e624dfe4c95c934df6a6bb3a77 +Author: Beining Han +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 4 lines last edited by Beining Han in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8877decbbb979041a9a8cab8908db7fce7e7fbe8 +Author: Hei Law +Date: Fri Jun 30 03:11:10 2023 -0400 + + Add 5 lines last edited by Hei Law in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e38f9f4458d19989da837348dbd5b18784aee242 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Zeyu Ma in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b2f51be6928c2e86244a0c1aebcb22e5c123d14 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75f22acae9dde135fe079a318371c247c257fcda +Author: Yiming Zuo +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 15 lines last edited by Yiming Zuo in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b2d85e129e8dc238ee2bb188fd682ec71609e955 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 63 lines last edited by Lingjie Mei in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 49b032c41d966753ff8cc972cb62087f64256c58 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 266 lines last edited by Alexander Raistrick in worldgen/nodes/node_info.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ae41925cabd74e1cc0969bf1552d080c52436321 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 42 lines last edited by Pvl-bot in worldgen/rendering/auto_exposure.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a5b1a676d9770711544c3d11aed3d3ef8a6a5668 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 51 lines last edited by Lahav Lipson in worldgen/rendering/resample.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 58407782cd0408e87f7beff0f64a8ea2161a5757 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/rendering/post_render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 08e685e3f215cd12afb5d963dda6b3d0404ef498 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/rendering/post_render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b5ee0c11f7cdba0f678c02be12b301ce3d7f30d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 74 lines last edited by Lahav Lipson in worldgen/rendering/post_render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 33b233b16c215b9bc326fba56b6bac134b50b207 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 14 lines last edited by Zeyu Ma in worldgen/rendering/render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 65984cfa57cbc3ff3bc06d23928ace33e6bcac92 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 15 lines last edited by Pvl-bot in worldgen/rendering/render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f0f3a8cf33d0ad299d8efb8ee71a53dba4e084f1 +Author: Hei Law +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 38 lines last edited by Hei Law in worldgen/rendering/render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 67333b50b04b81e582c170dc43cafaca220e2ae2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 89 lines last edited by Alexander Raistrick in worldgen/rendering/render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd2a39b7cc589eb8a46771c08480cceb14b9eec6 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 208 lines last edited by Lahav Lipson in worldgen/rendering/render.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 100c3c16d4b2c4d64626cff1949efcd8b922a2d2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/utils/cluster.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 45a0915a175dd71462964244f3ac7238b9400eab +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/utils/cluster.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 045475a689f131983a8c1f982950fef2ed07dc54 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 70 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/utils/cluster.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f61d5a6a306ec4f334318b7ba02147feaf18ff75 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/utils/selection.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fec38c344899b0490f9ed4e1b773312b9c361dec +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 23 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/utils/selection.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2c47b408217a9c68ddb2df4508d9c43737765821 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/utils/wind.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9be175819399476352a5e6e779dc42cec590436c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 56 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/utils/wind.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9516aa9ce4dd08dbaa75d1e1e557a1ec439218b3 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 1 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dd72aeeb82266b7f79f15fdef48a5137404ac7f2 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 4 lines last edited by Lahav Lipson in worldgen/surfaces/scatters/rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2c4d71ba90bf1dd6b99ec36ee750d4a4f5f5bffc +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1ce54adcbf43be0f2278261119a17131fcea9b57 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 24 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/rocks.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9a837b85014cd0b8b57ca25b7dd80db5177b085e +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/ground_mushroom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b6b547f7980f13c860dd9f7db1bfb61f0294599d +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 8 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/ground_mushroom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3de4154ff5b37acaa21aed22f6f9104dab1d4ac6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 19 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/ground_mushroom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b7f0fb3e246e9c6b679609441dc415dd25e06b14 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/coral_reef.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ce76a156b5070c770cf37da5b8535310ce54e0b4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 21 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/coral_reef.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d433cafd1328dece97b963b19b1b69ce4c94294b +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 34 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/coral_reef.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa547cca7ddd134bc9e06ea7a710dae45c6cb63f +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/slime_mold.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b7c22e4a50601091e911cad73a0315f0d69d1e4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/slime_mold.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d31dbd1b6f64d2ef460a5a9502d35e7d5db17278 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 39 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/slime_mold.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5c1af13e576caefff1c1018135308aa58305492b +Author: Yihan Wang +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 1 lines last edited by Yihan Wang in worldgen/surfaces/scatters/pine_needle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dc01e73a925e86f7611b3670bb34c307551da6a5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/pine_needle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9d1c8aea29d13363ad73b5b47ce25d8eb6228512 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 18 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/pine_needle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ff9f6df85452c4f42c1bfe14d6939c38dd2cd2af +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 82 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/pine_needle.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5e861cea4bd575ec18aadd87b47db2f6d7e48c15 +Author: Pvl-bot +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/seashells.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 75760ac1a896d966cc42702ffd9069ff24d47eb2 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 10 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/seashells.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2e7e882f2b9a142b643479565ba0f73db2a3c61e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:09 2023 -0400 + + Add 19 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/seashells.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9f136a45ede8c19b9c590720bc8e8ef7dbb7b23c +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 324ec5359ca121d497b32b7ba285857248ece9d4 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 13 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cd5d316dba33265e12f6b30f0eed03ea9d440cc6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/pinecone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0e3c3a11e8c4f55b6ba0a00ab77b323c411e9541 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9bd195553d2644ad8efa90f4760fc9dd56faf693 +Author: Beining Han +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 14 lines last edited by Beining Han in worldgen/surfaces/scatters/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9367f349a6221cf5e4f1b82f9e5599ea11cfbaeb +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/flowerplant.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f8ad90b40bf26f70c446d40b2aa00450afb9b802 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/monocot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 86d40c7902fcaa9c28128d82d4293c44b75a1929 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 9 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/monocot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bf25d99020f02efb029365a56c96564a4b11fa38 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 16 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/monocot.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 54cfb6d6d9927bb30c0ef032abd33c7b74910cd2 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/scatters/snow_layer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ae489aeb332e0e1b54f394fbaeade2e0978c2a62 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/snow_layer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 137e19e0fbe84c876028be4e5c74ad2927ca87ed +Author: Zeyu Ma +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 10 lines last edited by Zeyu Ma in worldgen/surfaces/scatters/snow_layer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cb792d450731b109b66ef3ccb40f7827beaf74d4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 13 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/snow_layer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 954f1a161ae894d0820c689d51589a6e9158c7aa +Author: Yihan Wang +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/surfaces/scatters/moss.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0fbd6841ac93e3d9a95f356191920f506226540a +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 5 lines last edited by Mingzhe Wang in worldgen/surfaces/scatters/moss.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f3897770d6fdf207a6f97456c09fcbd44d783433 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/moss.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7f6422752a8ee9294d467fcc7793757257fd59ee +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 26 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/moss.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa64b3af9b39295e89f5920b94cd0903dcc63e5a +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 80 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/moss.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa8227dbbb2466872dd04825ccc470e4e31e0ccf +Author: Beining Han +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 5 lines last edited by Beining Han in worldgen/surfaces/scatters/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cafe5f599cb9cd41dd8059da0374404ba08a24d7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit abd3f57677841656bc4f7248500463caa35b2646 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/fern.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 868657dad8172b916752a8a083aa170b20b222ac +Author: Yihan Wang +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/surfaces/scatters/ivy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3b220a679d9275664503cf7d907f78969602d415 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/surfaces/scatters/ivy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 333e9a409835ed7dc1aff54c3136ddaabf890975 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/ivy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3c12b37ce43627ea37976f3c034d1215d0ec3331 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 77 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/ivy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9d480cc480f7dc90a44ab1b93e7504ae78aef4e9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/decorative_plants.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0a81065e9d93113bc264d44cceb2b4fc478ebace +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 32 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/decorative_plants.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a3e06749b0d45d8d13278a34e112c75d4e3c24c2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/chopped_trees.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 777e919292eba5312e9f31b142d3023f857da4a0 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 7 lines last edited by Yiming Zuo in worldgen/surfaces/scatters/chopped_trees.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 34e2362a319ab2c4318c558d7109668c575283ef +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 159 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/chopped_trees.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d4b2fb0af2e38964c4a9341651ef6927a85855a7 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/surfaces/scatters/grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 53971ebc2f0d98cc35b5998928b4fb7216b3fd2e +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a6561bed0ccf0edb54568a38a363cd4e3b44ff7c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 42 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/grass.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c7120724229974bf125c23ca9e8be931cbc7f13 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7675da9d7102a048e0e43aa9590bff8e1783e2ad +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 10 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c1c612689ba87eb0f83aad34282ed713bbc638b0 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 19 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/jellyfish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e61124dc68d15bfbb67d687ee9a9db519d3cf2b8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/mollusk.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2b64878b777a44430c984ca4252b6de9fe1d8702 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/mollusk.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8a5ca717085fa08891519f1ed5d408a95b6472d1 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 21 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/mollusk.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3debc26397aa8ea3f390ea9bc0942197676ba39d +Author: Yihan Wang +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 2 lines last edited by Yihan Wang in worldgen/surfaces/scatters/lichen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ab80b51d6b3ecb335a139d00b6cb762f66939ad1 +Author: Pvl-bot +Date: Fri Jun 30 03:11:08 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/lichen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97ae73a84be1ea985395dd6578a0a56b21a968f7 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 19 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/lichen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 082d9afda3cc34bf7821226ebbe1b81fd35a4377 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 90 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/lichen.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f1bda4c09519c36df4ce2414d157067f5602a678 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/surfaces/scatters/ground_twigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0b37dd3a2648476278a1a0153a93214d8a2ad860 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/ground_twigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4fbf52731c8a475cf33631c83f6f3512e9d360d6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 33 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/ground_twigs.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2be81b718de7a94d0d8e7e1eac1915119bce680e +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cc56eb820f432a2d0409cb153d9504ef02abbfc1 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 10 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a577887772586274c9684a0534b6ae8bf5b7a921 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 15 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/seaweed.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 936530475c45f7d004b3572194215a0c3e0ca5fe +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit be1b1900296a03d64eb6acc2fc102bb0653029ec +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5bc5245bd27d0e939ebce04b5d7e05199096720f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 17 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/urchin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4532bdf203d1b64455bf9d54ab4acef5d80720d4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/ground_leaves.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2bb902609d94036c4cae7c68493e1fd4c5d32add +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 18 lines last edited by Alexander Raistrick in worldgen/surfaces/scatters/ground_leaves.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 11bfcc8068c4a2b0d78a9764420389fe81860fd2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/scatters/mushroom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d95a29b2cf66b2b247b826c88dbb68cfa3603c3f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 86 lines last edited by Lingjie Mei in worldgen/surfaces/scatters/mushroom.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5dfeab892a9b0f8113246e4dde1557c08eae7d39 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/twocolorz.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1de6bc9d0b478cab950dd3d29f90ac39aa063a2 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/twocolorz.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2626efd66d831c81e0ddbc47ec92f48de6d8c5a5 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 58 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/twocolorz.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7dcb954913c20e805c5799593997ac4f70d71d98 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/surfaces/templates/lava.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a832b811c59b8e494e1fab2ee2605f43ef37820f +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 21 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/lava.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 19130a7275d2bb3153b631839433c45f57a471b6 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 211 lines last edited by Ankit Goyal in worldgen/surfaces/templates/lava.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5de0ce1e3adb376b56002a241fdbc24781134623 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 222 lines last edited by Zeyu Ma in worldgen/surfaces/templates/lava.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2bf5593b668deb69f66d9a50fcb5e4b5efbfb13f +Author: Lahav Lipson +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 84572ca988b1418c201c4e3db11c24b5b274fff5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 95c450f91b5db1647d2840242a95072edfcca3c4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7df0a4da59d281c8dfb6f76ba7450fdfb5719ae9 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 22 lines last edited by Ankit Goyal in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c286e2d5f4f33db3460e7ff9f28d0ed861c637b7 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 69 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8714d43a0147c72f3ea900b3dd2749fd7e595a71 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 193 lines last edited by Zeyu Ma in worldgen/surfaces/templates/dirt.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6f33279f4543e5c4e1aa8670ba18854df6c52677 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/scale.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eb663783aee861d323ce1eb681815dc8aa449690 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/scale.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit da71467a56ce43daffdadb4c58a76aeb18cd8ab9 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 340 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/scale.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 40561175de38a7bec5e882faf781b30fb73bbd28 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/spot_sparse_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 993046f404af7f6a2cd2a5701eff21f93ea5e9e9 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/spot_sparse_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db7ae05407c59f260f199fae57f187ed82c948e6 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 123 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/spot_sparse_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0e8f8602df3be85d094a5925591f3541be5d9cd3 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 3 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/ice.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 62b8d858941f61f6c1118e5ef1abe1332004ef69 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 5 lines last edited by Ankit Goyal in worldgen/surfaces/templates/ice.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c410b11f1c89a32f8123b1050044df124a825c50 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/surfaces/templates/ice.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 13ea07fc4ea3ecf1d48a67a7469734e5b806acee +Author: Zeyu Ma +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 24 lines last edited by Zeyu Ma in worldgen/surfaces/templates/ice.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 94f70bf246b2e06c4887315b742c6dc45d61f896 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 78 lines last edited by Hongyu Wen in worldgen/surfaces/templates/ice.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8ba7562b155bdc69318fa23f29a56a02c3db6eb7 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 3 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/sandstone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8868ad4c51fba965fe6c328231d00fbcf542e957 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/sandstone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3ed71e51a5107ec50e498c30fbe220511f538fa2 +Author: Pvl-bot +Date: Fri Jun 30 03:11:07 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/surfaces/templates/sandstone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0823da266f77c0d267824a7892d00377352d25c7 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 105 lines last edited by Zeyu Ma in worldgen/surfaces/templates/sandstone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c8f4f0f63cc26c9602ade5b9d7e9e38cbd87cab9 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 495 lines last edited by Ankit Goyal in worldgen/surfaces/templates/sandstone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c40941d34c7058aca12acfdcb4cd9e6f2d34c099 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/nose.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2ac07cb2577a26bb36169cc172df8dafbfd0f275 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 37 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/nose.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e53fe486f1db0f37be55d2e8780c7ba532978c91 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/templates/two_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3bb8a57868c9eaea39f6eb9ca1257e082051f5b9 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/two_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 249dc2bbde40649de6a4785b88abe871d6f6306f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/two_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 19ee97418a082358d5ced514913f59b0bbec5de7 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 105 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/two_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 43b3ee6b5cd2fbc574ce4d7bc511b36fad6e8a14 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/atmosphere_light_haze.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 56aac172c81d12c31ebb5d809f53ee0a9fcd471e +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 8 lines last edited by Zeyu Ma in worldgen/surfaces/templates/atmosphere_light_haze.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3417c2aa9131ebd82182a185b12b72fc11813794 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 18 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/atmosphere_light_haze.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21500bc972b5b35cf73b9071f851cdb0e9d23a35 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/mud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1bf7a504ae12793322d6ec04a7aba7b72a698a13 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/surfaces/templates/mud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fc20e0bf5ee92225fa459d80a00b19d17f3e7b30 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 7 lines last edited by Ankit Goyal in worldgen/surfaces/templates/mud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit edb49f03b0cd77ab65db3eb7c0d96e5ba4421414 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 15 lines last edited by Zeyu Ma in worldgen/surfaces/templates/mud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 77e3401b3ced84c4b51a137714e3b17fa8a5f3a7 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 119 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/mud.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 07b27d9bfe975292cc60443495097fe6837344a4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/eyeball.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 52ad554eaa91da7ce2f60edbbc662946c939e8c5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 33 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/eyeball.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed8e663b99b4f81dcf84a408fe236a15d22c4381 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 56 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/eyeball.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1f67e417f3d66a99e546e3a5d5fdd1d5abaa87cc +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/chunkyrock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0cad2be337fb4cadf91292be2915e72fa11185b5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 7 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/chunkyrock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c147245fe19241b43988c97bfefeee1ebf28e509 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 67 lines last edited by Zeyu Ma in worldgen/surfaces/templates/chunkyrock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5728ad511e31ef25eeb02a6a3080b6e5253843f3 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 74 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/chunkyrock.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 742c549d36b74d674447f2ada6a95ae41563e1af +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/snake_scale.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f28ced2bc3859a0bf89fedc14768cb996ae9acdb +Author: Hongyu Wen +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 272 lines last edited by Hongyu Wen in worldgen/surfaces/templates/snake_scale.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f83f1cec4c515866c2bb9c77c0fe1d02e03f1abd +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/surfaces/templates/bark_birch.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7b285c9fb5e04913ae9a448ee1b64893695c49d0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 14 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/bark_birch.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5bb02f63430703016e71623952b24834c3fc5006 +Author: Yiming Zuo +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 190 lines last edited by Yiming Zuo in worldgen/surfaces/templates/bark_birch.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efdf2677c9c41123c872a076758ba4280454fb7b +Author: Lahav Lipson +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d60f0449b764de403b8543058323d9173b8ca374 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fce03dfa1b4cb99e59818b71789d3d64be864048 +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7ed0c1f3053b80273abe477e83b660f7452d8522 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 12 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 244d6efe25d0514e6e195becff30e070d81dd895 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 18 lines last edited by Ankit Goyal in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ded02940ca08a46ff9e4ca8a0dcd03ec67359436 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 263 lines last edited by Zeyu Ma in worldgen/surfaces/templates/stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fe4158ef4686b509c3d6005b6ce1f49d0919b314 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/cracked_ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c5b890bb626f6511c20b8fedfc92f3e0a64782c1 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 5 lines last edited by Ankit Goyal in worldgen/surfaces/templates/cracked_ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 593a02ca5c485a72f414dee2fbd754e4967f4d8d +Author: Pvl-bot +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/cracked_ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 840366f9a8eb0606b5a6bebb3c089dc7b33036ba +Author: Zeyu Ma +Date: Fri Jun 30 03:11:06 2023 -0400 + + Add 18 lines last edited by Zeyu Ma in worldgen/surfaces/templates/cracked_ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 245302a8d663bbfc711323978f3ddb9f9cd792ad +Author: Yiming Zuo +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 166 lines last edited by Yiming Zuo in worldgen/surfaces/templates/cracked_ground.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0cd93371efd089e8c14af843a91c5ab9cbf90a9a +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/simple_brownish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5ced5fe6e09b268998565717d8d5f219ffd0bb37 +Author: Beining Han +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 27 lines last edited by Beining Han in worldgen/surfaces/templates/simple_brownish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 858d97c8a72e52e297652000e1abad40a07d56bd +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/bone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a06ab6344ed2f1d54011379b6307572728d25acf +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 20 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/bone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 85b9b0bf74988ea77f6ec7f6aa95b2d0188017fe +Author: Yihan Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 60 lines last edited by Yihan Wang in worldgen/surfaces/templates/bone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 748223bf29b0841828f95ec341419241b3b6b7e8 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/tongue.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 66a0c18ca1d974ddedd541f5d7ec3f111076771a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 29 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/tongue.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 22406b9b177770902b224ea76e9969167822ef13 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/soil.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2258f02d77ad425ff35f57d744e4f69cb70afd5c +Author: Zeyu Ma +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 141 lines last edited by Zeyu Ma in worldgen/surfaces/templates/soil.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 92203a38b2e23960266ee0ba88ce1156e3ece72b +Author: Ankit Goyal +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 182 lines last edited by Ankit Goyal in worldgen/surfaces/templates/soil.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a60c5268b60e9e800f6aac12ee0cdda508875e02 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/templates/slimy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1b0af9b19ddaf011fc8ac10d89dc83f10471673f +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/slimy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ea1f5013b2ba07b276f9a7054860d5315bbd4001 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 14 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/slimy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 47c34d55910595d556dd70bc662578b4f7f20781 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 106 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/slimy.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 33c4a6b23dafcd49649540d51efd55ab25318200 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/snake_shaders.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4b88edd6b309959551c8b62c2d221a883e8b30e7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/snake_shaders.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 397201770a117f013ca3f0bc31a6207d976e7a0e +Author: Hongyu Wen +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 297 lines last edited by Hongyu Wen in worldgen/surfaces/templates/snake_shaders.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 429f92562367d14d3b0ed5401e573b428a3956bb +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/aluminumdisp2tut.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 12973dec061d29d63d767c68b2326a1f93ff4043 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/aluminumdisp2tut.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cae0380c81ed2daa2098145454e79f8d5ebfb72c +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 192 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/aluminumdisp2tut.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c2aa38b56e3249c3b98fe64707988f4264ff1e96 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/reptile_brown_circle_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 283f914b642e8d8b601ecf32e26577e63fb7dabc +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/reptile_brown_circle_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 336a46056b01c412ff7f102c362e894915b8316b +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 299 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/reptile_brown_circle_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1a4701812225deb0d3f8304a365e818e9971c42 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/face_size_visualizer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bd794a3d92040ae93f13f556b50982f04f4c9464 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 41 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/face_size_visualizer.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9063d366099d7ea59448bc6f144628f7d2dc8650 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c94bc446ac855c8b944834d72b73cdcd14a07367 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d8e7c2968ed0c0b0c03c55f5e2244fae1440ee56 +Author: Beining Han +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 45 lines last edited by Beining Han in worldgen/surfaces/templates/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af30523c8bbaa2f983a1423aa22898fd02d616af +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 452 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/bird.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit daf294c3a329028db79fc941e215223e87dae9f0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/__init__.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 21288c5052b103f3df8d97238aebc456749919a0 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/surfaces/templates/sand.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cc2923879bda135284e6578ece95126c978e62f0 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 3 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/sand.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c83d7d421ca4057ca8f819fea714d4e3e8414735 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/sand.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 515dff4e6868079abeabf58d3d45bf038a46633f +Author: Zeyu Ma +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 126 lines last edited by Zeyu Ma in worldgen/surfaces/templates/sand.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b11ed0fcf5101230cc173c8664013ef3597d3490 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 5 lines last edited by Pvl-bot in worldgen/surfaces/templates/bark.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 59277ff73496b1c9fdd704d44f94a2cd1aec0afc +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 7 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/bark.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 08506e74726ee08318a3247d6be378847234fba5 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 123 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/bark.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7626580c07a4a85722ddc4bd9a95257beaabaec5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/fishfin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ccd4336df8833556a28c990fad30c6a118c3d89b +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:05 2023 -0400 + + Add 262 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/fishfin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 46fbc73a8dab688e229df7bb082ac980a78aad44 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/three_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d72789c9ac18f94d85d9e31b90ccfde70888c654 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Lahav Lipson in worldgen/surfaces/templates/three_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5f6e7a46447a82ece85257bd78043f850a5f9280 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 18 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/three_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 86393c66f85da428b5fe0a58bbb000172998642d +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 161 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/three_color_spots.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c6c24ebf3ae52a4b87c2fb4a61b9faa2b4067c4 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/reptile_two_color_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4e79be179d48280c9fa3e2c0de80b158e5a3e0cf +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/reptile_two_color_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 78972001d77851b00d95bcf2310788b93c07ad0e +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 234 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/reptile_two_color_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a02cc9fb7ec8b77fce9507c010441f9265dce633 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/simple_whitish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 971b77a06e38acda64c494cd0ae75c7cf0f7f69b +Author: Beining Han +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 28 lines last edited by Beining Han in worldgen/surfaces/templates/simple_whitish.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 71b9d1a3bdf2d5a00c3a12c7bf1c3acafe03a4dd +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/simple_greenery.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74e199d97d72f29a21b19ec86664eb23cc6ba062 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 38 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/simple_greenery.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2ff5d9681c5afe16cfffa50a9425c717390e4c43 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/giraffe_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a8c0a6953766bca0f750f116519e65b00934ac51 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 15 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/giraffe_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 76c33014a23d6d267821d31e34e1234dcb25db41 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 81 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/giraffe_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6297622c1fbff5264ae12c317140199728961720 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 70 lines last edited by Hongyu Wen in worldgen/surfaces/templates/beak.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0193a43b3d6c0e863e08cd93958cbdaa8ac2cec0 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/wood.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3f80c8ce4e0b229b5e786949706c5e9d2566ac46 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/wood.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e4b2ac26e109e38377804a59f193c99f080b56be +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 77 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/wood.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f55642e377dd22fc6bdc8dfde73f6d898574dfc5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/tiger_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7cd2fd5e127a043a28ec216c43097685a6302e86 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 47 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/tiger_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bc2a84190bbe4418be476e0a543b65dd4909c015 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 104 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/tiger_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 99bc849ddc856c8c1e653877481860d79efe5550 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/reptile_gray_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7d7783347361f5b0ecc762c490af4c9be899eec +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/reptile_gray_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6f0e0fee6d8d667d59e65eec2237ff13e70402ed +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 175 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/reptile_gray_attr.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9f4c37e6bd03f81c5262dbb2f9e7ac20268362be +Author: Lahav Lipson +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 3 lines last edited by Lahav Lipson in worldgen/surfaces/templates/mountain.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a0c0388acdd8b4845ece1657542c9332d46f86d5 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/mountain.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 215bcbce239f37a6b83658d9f1e7eebdfda95bfc +Author: Ankit Goyal +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 11 lines last edited by Ankit Goyal in worldgen/surfaces/templates/mountain.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 37ac68295b2722fc5a124048e60cafc6f8776032 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 16 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/mountain.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7fde8bf5d46fa12ac131979f599534d85c5a1d1e +Author: Zeyu Ma +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 280 lines last edited by Zeyu Ma in worldgen/surfaces/templates/mountain.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f37ac2d07b2bc4d28b9aea57037ac66f8a8f1129 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/fishbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c4c2aee94c0f0174d4ad45858506260138e7583 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/fishbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d058d72f82c478a67481cd0aa063ddbe13862154 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 1026 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/fishbody.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4da7a8ca09262e4b37a6a7fb41d9ab3e51bdbbfd +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/grass_blade_texture.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6de507903f46aa30f0184ca31cc0ff4779983673 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 19 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/grass_blade_texture.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit efeaf45d739da469db923c7505b357b00551c473 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 196 lines last edited by Lahav Lipson in worldgen/surfaces/templates/grass_blade_texture.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 908178dd4138fcc72f09b4590801290ea696a0ef +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 4 lines last edited by Pvl-bot in worldgen/surfaces/templates/snow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5692341f99740a41e7c1cb18b8ac0eaefc7b0093 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 18 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/snow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0c81bc41c66e3e2177ed9d3fc2ed5e9569422faf +Author: Zeyu Ma +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 112 lines last edited by Zeyu Ma in worldgen/surfaces/templates/snow.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0135eba9709d7ed0c757de65b4aa55969f1cae47 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/fish_eye_shader.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9ae7d54351b333d3656719d5a783495f0ca37309 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 207 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/fish_eye_shader.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9545c617180e22f16e1afce3ac29bdcf5d9a6909 +Author: Pvl-bot +Date: Fri Jun 30 03:11:04 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/chitin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 97c40b03f7243aca18a01f781f742bb044a614cc +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 93 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/chitin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2f75fb0e44e09e08ca36ae119c652f85fdd21dea +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 125 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/chitin.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 01928f544135739e9566d6664b2f27d5b192c765 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 7 lines last edited by Pvl-bot in worldgen/surfaces/templates/bark_random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9fcef67c55ac1a2ca7e4fcaef844982b9c5f4a29 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 23 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/bark_random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af0bc2a1327c4199003a20a7b8626df0051cb32a +Author: Yiming Zuo +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 519 lines last edited by Yiming Zuo in worldgen/surfaces/templates/bark_random.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 040e2dff6750c57e077743c6c928065ec706e1e6 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3ec32ee0468d6447970588c5a7c67166c93eee63 +Author: Yihan Wang +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 78 lines last edited by Yihan Wang in worldgen/surfaces/templates/horn.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c612986c76dc68196dc0e3c26399b41517910e8b +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d1a0dc6aa9d8a421d189d60fdb2f9b14a121c06f +Author: Beining Han +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 314 lines last edited by Beining Han in worldgen/surfaces/templates/succulent.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2dd7e80bcc27009bccf3f40414005795e10bafb3 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/surfaces/templates/water.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3d3423b0f0a2e2ac047d3b6df7fbea3ce5c6fd5f +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/water.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1e6e0995b60347cfc3b009c9470694523696c502 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 36 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/water.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f21f836c13a4e35ac2b3c5027e4e86f31caaf0ae +Author: Zeyu Ma +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 204 lines last edited by Zeyu Ma in worldgen/surfaces/templates/water.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit eeef5630194972d9c269ee0314fdfa97b117f31f +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/basic_bsdf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1226702aed4bd2cbce0553a97374a570bfba269a +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 31 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/basic_bsdf.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7f7f25cc54096e7ae79b51c216e97f8e4f7c7858 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/surfaces/templates/cobble_stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0e3f0ae88f8633643ecc4864dadcbb39e3ae0ae4 +Author: Ankit Goyal +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 2 lines last edited by Ankit Goyal in worldgen/surfaces/templates/cobble_stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 460612be65d44b99f0049d4be110f0e11970b07e +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/templates/cobble_stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d9d67b502254cb77f0708c6551c9ba7749b6f97c +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 25 lines last edited by Mingzhe Wang in worldgen/surfaces/templates/cobble_stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1413534216d53a4263d8e88a5ce5a0bab2a2caae +Author: Zeyu Ma +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 122 lines last edited by Zeyu Ma in worldgen/surfaces/templates/cobble_stone.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5769658ce26a28a0ab12aac34011eef9c6b6fba8 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/surfaces/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1efad5e7a4e69ae1e53c51870e503375f19668f3 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7e592e14b2a5f591a14405723d66a7ed3e0cd15b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 37 lines last edited by Alexander Raistrick in worldgen/surfaces/dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 519c638ffb2b05ebd27d0cdb9236700bb3bf3390 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e75d140c70512ca99fb3bbd05b0555dc0e1666b4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 787864c5e5c618f597bd828843b6ddb5dc280262 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 47 lines last edited by Lingjie Mei in worldgen/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1aa4e2a11de017e39640fb78aceb0a3218f37f8 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 124 lines last edited by Mingzhe Wang in worldgen/surfaces/surface_utils.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fc8a12578b8156ab998f31e21a4dc79db038b5a4 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 6 lines last edited by Pvl-bot in worldgen/surfaces/surface_mixing_dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3a2fc31dea210913b544d3d0aaab709ccacb072c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 12 lines last edited by Alexander Raistrick in worldgen/surfaces/surface_mixing_dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 19a7ee1060584d7f241c5fb29ae54047275198a5 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 28 lines last edited by Lahav Lipson in worldgen/surfaces/surface_mixing_dev_script.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 72092a0958836a3b84683370fe4c764da74fd211 +Author: Mingzhe Wang +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 1 lines last edited by Mingzhe Wang in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 602eedebb5f2cb9a173a310e5bb171b2ba4ade0d +Author: Karhan Kayan +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 4 lines last edited by Karhan Kayan in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 17221888b1acce0dee2340771a4d711163359870 +Author: Hongyu Wen +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 8 lines last edited by Hongyu Wen in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9418ed9624d8961a5589431f09ba582f700dd275 +Author: Pvl-bot +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 22a3e4d3629494903b83beefa72878d0f078b218 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 16 lines last edited by Zeyu Ma in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d6e3ccad7eeed58ed576616dd6291071cee8a90 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 25 lines last edited by Lahav Lipson in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9a830bd197aa891aac1a18e3c779a0e86af0090f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:03 2023 -0400 + + Add 111 lines last edited by Lingjie Mei in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d461618db13bc656c894329bd653d03c5bc38880 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 234 lines last edited by Alexander Raistrick in worldgen/surfaces/surface.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 61d0fba2d0d8602817ac584f1165f56aa83267fc +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 3 lines last edited by Zeyu Ma in worldgen/config/scene_types/plain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 039b1ffc5988328766ab7a6d4371a152aa170f92 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 25 lines last edited by Alexander Raistrick in worldgen/config/scene_types/plain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 788dfc9893d61a82856926b0684a13e1e0d6374d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 26 lines last edited by Zeyu Ma in worldgen/config/scene_types/cave.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 945f507be7b670fa1c16bac91fd1f6a302a501a8 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 35 lines last edited by Alexander Raistrick in worldgen/config/scene_types/cave.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 631747f074cbb4246f84090a20cdf145536c3905 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 13 lines last edited by Zeyu Ma in worldgen/config/scene_types/river.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 05d26f6f3b8a146fcaccfdce4938c50068cbe081 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 28 lines last edited by Alexander Raistrick in worldgen/config/scene_types/river.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9885969f9866e0f175f104d8007cc92f41a3ee68 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/config/scene_types/cliff.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a337da3df6bfe7d79b6df1ad644cf7dd4f832d3e +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 14 lines last edited by Zeyu Ma in worldgen/config/scene_types/cliff.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ec49d709edc69ed456fba6950384ac90bf4eaa69 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/config/scene_types/coral_reef.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e1e49c691d538d52e19f2949dafd6971aeb32d44 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/config/scene_types/coral_reef.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d33be963192e5e8eb8cbe19f96d36e69bc8dc887 +Author: Pvl-bot +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/config/scene_types/desert.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fa2800f9b13bc0d16575c1323c442b3fd710b1b3 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 1 lines last edited by Lingjie Mei in worldgen/config/scene_types/desert.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2830c81623c2354f92607839107e70b9b56943f1 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 18 lines last edited by Zeyu Ma in worldgen/config/scene_types/desert.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5a75d33a7ead9d3d3342825470bb5639de5060d5 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 25 lines last edited by Alexander Raistrick in worldgen/config/scene_types/desert.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6227818ba1e563e6ec80a926da1bb4c677b83306 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 4 lines last edited by Alexander Raistrick in worldgen/config/scene_types/snowy_mountain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ace50ece997286cf87fcad3be54520fee3d1be47 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 31 lines last edited by Zeyu Ma in worldgen/config/scene_types/snowy_mountain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 57b1c3e114abff81fc147ce31f553bedc44b1127 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 13 lines last edited by Alexander Raistrick in worldgen/config/scene_types/arctic.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3bde811030dc78f6ed769af01fd3b03c9416dced +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 28 lines last edited by Zeyu Ma in worldgen/config/scene_types/arctic.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit db282685ad7ae5dcc80c66bdd3a2f2b91ce5e60c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 11 lines last edited by Alexander Raistrick in worldgen/config/scene_types/coast.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit adbf76e58a503f8f75b745883026341a5f822549 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 32 lines last edited by Zeyu Ma in worldgen/config/scene_types/coast.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 29fa1c020cfa6e01d3efcbde633dc75c0d5e8a5d +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 3 lines last edited by Zeyu Ma in worldgen/config/scene_types/forest.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 27e04967cea3d6fb39f54d0f5501c7acda64d06e +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 55 lines last edited by Alexander Raistrick in worldgen/config/scene_types/forest.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c0f1b1f8a8889b60c001c6b463f8d738528306f0 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 8 lines last edited by Lingjie Mei in worldgen/config/scene_types/under_water.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 36d0faa13320b747cc89c6f95e52735df3d50715 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 13 lines last edited by Zeyu Ma in worldgen/config/scene_types/under_water.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 86c88747f903cff6b31e60807b32f34c7d0585e8 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 63 lines last edited by Alexander Raistrick in worldgen/config/scene_types/under_water.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d56d700da699e4f6b79a459dd51678c5080f7c34 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 5 lines last edited by Zeyu Ma in worldgen/config/scene_types/mountain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 203a92b35e1de4ca94c7803aa6755004aefecaaa +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 8 lines last edited by Alexander Raistrick in worldgen/config/scene_types/mountain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7d991ca45b30bd310de1c5210ac9f082b3dd5671 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 2 lines last edited by Lingjie Mei in worldgen/config/scene_types/kelp_forest.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 737db271662d3b143363c5b283e704d17d931ffc +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 22 lines last edited by Alexander Raistrick in worldgen/config/scene_types/kelp_forest.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74cdc70dd13b00c8a469f444da46ae081748c192 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 7 lines last edited by Alexander Raistrick in worldgen/config/scene_types/canyon.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9766ddaa080b9d7259d46a001fb9a56fdd5dde38 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 8 lines last edited by Zeyu Ma in worldgen/config/scene_types/canyon.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit cf12a5f35d26493711fb2843779c7972022ea2d9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:02 2023 -0400 + + Add 50 lines last edited by Zeyu Ma in worldgen/config/palette/water.json + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1f2d6c98947c54dfb13bd306c9c86a69d5be43bf +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 50 lines last edited by Zeyu Ma in worldgen/config/palette/sandstone.json + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8a857f4b2ff4427d9f3b842903ec0dda0a30c6c3 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 50 lines last edited by Zeyu Ma in worldgen/config/palette/desert.json + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6c0f48349c2af0c09071f4c3c7dd2f7eaee18ab7 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/config/simple.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1da542585359653c70ed081ae71e82d7f61a20af +Author: Pvl-bot +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 3 lines last edited by Pvl-bot in worldgen/config/simple.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3adc2dfd329ab3f1d6e789812f056010965e75a5 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/config/no_assets.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b398dae1ac9625f1f2f61c1fc37a55de0ed1fa1f +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Zeyu Ma in worldgen/config/no_assets.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d5647b3a843f87a5d2550db37d851f69aaa5506c +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 45 lines last edited by Alexander Raistrick in worldgen/config/no_assets.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 655d87435e7e500a54e1c50bad5d2fddcc2e0ed3 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 10 lines last edited by Zeyu Ma in worldgen/config/reuse_terrain_assets.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 79785b66a09027e4dbffb17a9fdaa59ee30bad9f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in worldgen/config/high_quality_terrain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9215f1a267408f4a8fddc8099e767b1e189a3a10 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 4 lines last edited by Zeyu Ma in worldgen/config/high_quality_terrain.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b54522163492f4457cb5067ba2496c3a2a8f380e +Author: Pvl-bot +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/config/stereo_training.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c1bc8163ae4ae1e0d306510d65631b6287b9992d +Author: Lahav Lipson +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Lahav Lipson in worldgen/config/stereo_training.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c9befacf759ddd9db6b8777c1521786ed55d4cc4 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 7 lines last edited by Zeyu Ma in worldgen/config/stereo_training.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 063da5f62336f2d98686a05363a352a095abc37f +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 17 lines last edited by Alexander Raistrick in worldgen/config/stereo_training.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6390ed70dc5b055754a0e5ee540aab869fa68538 +Author: Hei Law +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 1 lines last edited by Hei Law in worldgen/config/base.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0489251219c0b234e622babbfab9660a845dd316 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 32 lines last edited by Zeyu Ma in worldgen/config/base.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ba85630aecb42a3de64a2b46cfbc2c6f36e394f9 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 34 lines last edited by Lahav Lipson in worldgen/config/base.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 34964cbb63dbd4af6f0658aa6f5119a12537c0cc +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 145 lines last edited by Alexander Raistrick in worldgen/config/base.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit af684d282f645a7c7702b85849a106395fba8c97 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 9 lines last edited by Alexander Raistrick in worldgen/config/asset_demo.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2ddc977810d21f81a8e1a3cb56c38b57072511a4 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 3 lines last edited by Lingjie Mei in worldgen/config/base_surface_registry.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fafb646b1636620244837b7d1f71b9ac1d03764b +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 31 lines last edited by Alexander Raistrick in worldgen/config/base_surface_registry.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f4ca48f85f0490066f4646332c95cbbc9bcc6989 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 33 lines last edited by Zeyu Ma in worldgen/config/base_surface_registry.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 2e142d087789f5e02c1ccd19980af4bbb7751167 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 4 lines last edited by Zeyu Ma in worldgen/config/fast_terrain_assets.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3f095f0f12c5d8c34efd554a15626411ea890d73 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 3 lines last edited by Zeyu Ma in worldgen/config/monocular.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 67509281d99cd5c0ffe339508859674e9cdb3e22 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 5 lines last edited by Alexander Raistrick in worldgen/config/no_particles.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit abca0c4c5b8e106311abc9fb5466948084963fb7 +Author: Pvl-bot +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 8 lines last edited by Pvl-bot in worldgen/config/dev.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9c9f060e13f1cbcca893462893391e386a4409fe +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 8 lines last edited by Alexander Raistrick in worldgen/config/dev.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 855bc9ac0c52f274b4122af550515b1690a93b5f +Author: Pvl-bot +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 4 lines last edited by Pvl-bot in worldgen/config/no_creatures.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 08b10e8f2615df1878ce7cd5ec54a8a24ffdade6 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:01 2023 -0400 + + Add 2 lines last edited by Alexander Raistrick in worldgen/config/no_rocks.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 15b15727441cec53467c0d9b5b1bd499d5ef8991 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 20 lines last edited by Alexander Raistrick in worldgen/config/trailer.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6298ef51aca1e810abf7cd8749737698700f9e89 +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 1 lines last edited by Pvl-bot in worldgen/config/natural.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5b9d22d3d6b98dd205fd652a2d90db6e5716e7da +Author: Zeyu Ma +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 2 lines last edited by Zeyu Ma in worldgen/config/natural.gin + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 613e519dcb519c090bffa24063d6a533b739c47c +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 3 lines last edited by Pvl-bot in worldgen/asset_demo.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e97a625d33a0014d832cc8d1354b9447627fceea +Author: Zeyu Ma +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 26 lines last edited by Zeyu Ma in worldgen/asset_demo.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 657bce9c8ca1a0180843c79d384ca3d1418e5510 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 181 lines last edited by Alexander Raistrick in worldgen/asset_demo.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6a733761646cf0848bf72b0e3cd752704e26edc9 +Author: Hei Law +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 2 lines last edited by Hei Law in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 18e94e177032cf3c1aab409e74ecf08bf6f37fdc +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Pvl-bot in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit dedda5be5a670f67c61449302babd02f62f08a1b +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 11 lines last edited by Lahav Lipson in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7a1084c19eae959d2312f964d4103e513ace7b3f +Author: Lingjie Mei +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 17 lines last edited by Lingjie Mei in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c1c4311793cdb1a6def1d22aeb77e3e38a2a9d05 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 28 lines last edited by Zeyu Ma in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ecdfbd13f2b06c2bd2a9e31d6b70d03e2e0dd579 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 347 lines last edited by Alexander Raistrick in worldgen/generate.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9a4fd05ae7a7951f2d2f0525953f2e19ca1171c9 +Author: Hei Law +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 3 lines last edited by Hei Law in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9d1537514b0ec24c430437872d808fe202a56715 +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Pvl-bot in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 58b17f6cc3e86623659f7a3ffedb474579fce608 +Author: Yihan Wang +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Yihan Wang in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 46bb4a6dee1bed4041130ee991cab67a71ead1c5 +Author: Lingjie Mei +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 22 lines last edited by Lingjie Mei in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b8d90df1927b59384e6d199ffab2280a1c9f5ef9 +Author: Zeyu Ma +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 44 lines last edited by Zeyu Ma in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 686d6284b1b0b1452c1e6dbabe4b2dd2de7a0998 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 52 lines last edited by Lahav Lipson in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e788179213b27220067ca34d68672cf9452d4142 +Author: Alexander Raistrick +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 303 lines last edited by Alexander Raistrick in worldgen/core.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f858026d982604f494ca3fb22d2f6ac4580c4a3d +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 2 lines last edited by Pvl-bot in process_mesh/glsl/spine.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3341e95014a3c5bc1b4c51b7f7f48bc72178706c +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 52 lines last edited by Lahav Lipson in process_mesh/glsl/spine.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5f8f2185306f6b4458f92ad15fec978c2de9df96 +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/spine.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7265b275589673f1513bfa09869d3b6c26a7ac56 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 5 lines last edited by Lahav Lipson in process_mesh/glsl/spine.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d57fec664363384998510a2a5a879fd516128098 +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/next_wings.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 198d1187ce803fdb258762bee5ba8ae898ca1e92 +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 50 lines last edited by Lahav Lipson in process_mesh/glsl/next_wings.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit b67778daa2d706ec9f4e6c1841e1aa4765bf690c +Author: Pvl-bot +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/hair.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5e5d5d4fce9db382546316be88b23e7f63f7093c +Author: Lahav Lipson +Date: Fri Jun 30 03:11:00 2023 -0400 + + Add 32 lines last edited by Lahav Lipson in process_mesh/glsl/hair.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 74cea049d5a5d8c8a34f7ceb4d6f7406bd0522bf +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/wings.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a91a473044483be90e83c9cf6c363024915167a2 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 49 lines last edited by Lahav Lipson in process_mesh/glsl/wings.vert + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5e2d127c785d1948df149ae13cbacfdde09dd5eb +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/wings.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 433bc23e983a24cbce091894b2bae5427ca93bd2 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 56 lines last edited by Lahav Lipson in process_mesh/glsl/wings.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 10c35d608b2e2744c07c5210bac4887c906f4c32 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 2 lines last edited by Pvl-bot in process_mesh/glsl/hair.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 82c2104c4951da65d98360b4aea2717b20b2c8f0 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 100 lines last edited by Lahav Lipson in process_mesh/glsl/hair.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 17e5dbf900de45f5dcd4c6cba76db2dcbc915a11 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 2 lines last edited by Pvl-bot in process_mesh/glsl/wings.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit f1fd1f887e8a52e97aa7ab1001859b9430987ccc +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 104 lines last edited by Lahav Lipson in process_mesh/glsl/wings.geom + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 157d46939eb30b3467ef44a515a54da23af073cd +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 4 lines last edited by Pvl-bot in process_mesh/glsl/hair.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ee01661ad067fb4d1c58d052a913cc0355120f33 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 57 lines last edited by Lahav Lipson in process_mesh/glsl/hair.frag + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c0091f92d399cf66f35ac471e66cf0c0581367ca +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 14 lines last edited by Pvl-bot in process_mesh/utils.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 65bb67d8b6b27b3661e237280ebff0634bb1a6f9 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 28 lines last edited by Lahav Lipson in process_mesh/utils.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a0d6c08a5fb5f88dde2146322931c0a812f31178 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 36 lines last edited by Pvl-bot in process_mesh/main.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1737461c9614c32418dc3a6a2380620deb132acd +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 466 lines last edited by Lahav Lipson in process_mesh/main.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 6de4841646e9918984dc3bbf5136d287e57abc6c +Author: Alexander Raistrick +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 1 lines last edited by Alexander Raistrick in process_mesh/CMakeLists.txt + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ebc5d53b491d2a435fee5541f3f12a1f0baddc43 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 63 lines last edited by Lahav Lipson in process_mesh/CMakeLists.txt + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4078562f8255760aa1b0559b5459c653ad2e9f39 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 17 lines last edited by Pvl-bot in process_mesh/load_blender_mesh.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ed074bd1d01007e92e140e5af1cfbb93e781b478 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 90 lines last edited by Lahav Lipson in process_mesh/load_blender_mesh.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit e25bfeb0360d491ce0fd2ff74e026c1ccb21db2f +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 9 lines last edited by Pvl-bot in process_mesh/buffer_arrays.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8d6197cf3755ad019a0d08b43bd48da723d44ea5 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 117 lines last edited by Lahav Lipson in process_mesh/buffer_arrays.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit be730db2ae4b1753f5aa7760f8b11dc28bc365fc +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 10 lines last edited by Pvl-bot in process_mesh/string_tools.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 5be114b4f28a03eb0d1a39ee298e16d9f193aea9 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 31 lines last edited by Lahav Lipson in process_mesh/string_tools.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9e9058a7111f13c301c21ec8f277480a0b54cbcc +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 12 lines last edited by Pvl-bot in process_mesh/camera_view.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 028cc4f75d2a3515021c5bf7765cb3a794058d24 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 24 lines last edited by Lahav Lipson in process_mesh/camera_view.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a817f1abedff14fab35fb2b3164f3b55d501c7a9 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 14 lines last edited by Pvl-bot in process_mesh/io.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit bff6f301f8941a02e5e6f4be91dd518f51644331 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 47 lines last edited by Lahav Lipson in process_mesh/io.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit d7ca7a0294fb19407ca230daae14c2ccc6f2ffdc +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 12 lines last edited by Pvl-bot in process_mesh/camera_view.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 79bd829cbf4cd202b5cde8010de202d4e325e9ff +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 116 lines last edited by Lahav Lipson in process_mesh/camera_view.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9620bf42cf051b6fc541f266f724f9a2c704aa91 +Author: Pvl-bot +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 6 lines last edited by Pvl-bot in process_mesh/show.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 1a318ea77d09e7184064e932e5b80c8492d45cd5 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:59 2023 -0400 + + Add 50 lines last edited by Lahav Lipson in process_mesh/show.py + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 76614a208ddd8a34684f4879a09602426389e4dc +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 288 lines last edited by Pvl-bot in process_mesh/shader.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7be3e848014e4b10cb158aa54e391c90be8296f8 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 15 lines last edited by Pvl-bot in process_mesh/blender_object.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0d721679bf34c69af0e6b3ed8ec71b98f680a732 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 68 lines last edited by Lahav Lipson in process_mesh/blender_object.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 0317ee1a97004a364718866bb3f572f339cea12c +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 15 lines last edited by Pvl-bot in process_mesh/utils.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 3d0e5f41c695f86d5b4248f01d4d6d5ba4f7abb6 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 152 lines last edited by Lahav Lipson in process_mesh/utils.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 8bfe62e7a2af4d06601ff0d392cfe423c98e78f9 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 11 lines last edited by Lahav Lipson in process_mesh/load_blender_mesh.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fe7356b98a39e9595a5eea3d39e2bb41c45b4244 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 24 lines last edited by Pvl-bot in process_mesh/load_blender_mesh.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit c1846b7d90912cf9fa8ccb7cc844dce5a6e733d4 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 77 lines last edited by Pvl-bot in process_mesh/shader.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ae41e261f6ca62b1436a33cdc7eb5f0d9515e542 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 9 lines last edited by Pvl-bot in process_mesh/string_tools.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 23f38b7cd4fef89ef757c84d15e33f62de35e2ab +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 9 lines last edited by Lahav Lipson in process_mesh/string_tools.hpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit ca1611bccfab7ca8612793a09735d8d1961ac6c7 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 7 lines last edited by Pvl-bot in process_mesh/buffer_arrays.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 409785366db24f3cd6bb6a55145d625885d7a3eb +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 127 lines last edited by Lahav Lipson in process_mesh/buffer_arrays.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 4ce3c92de4d366f3c1d469162a97e014cd5fd9e2 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 20 lines last edited by Pvl-bot in process_mesh/io.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 9c81b0b573afd65976da018560a9171c8cdb0879 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 64 lines last edited by Lahav Lipson in process_mesh/io.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit a9b664c47ae75815726dd7de420b27d8d5df96dd +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 15 lines last edited by Pvl-bot in process_mesh/blender_object.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit 7135764600c01a5d71dc8b8de66b40b3744e4998 +Author: Lahav Lipson +Date: Fri Jun 30 03:10:58 2023 -0400 + + Add 185 lines last edited by Lahav Lipson in process_mesh/blender_object.cpp + + Commit made automatically to show authorship. This version of the code is not usable. + +commit fd99ac2314e85e2b7b5c9ebffb9107e2087b1902 +Author: Pvl-bot +Date: Fri Jun 30 03:10:58 2023 -0400 + + Initial commit + + Commit made automatically to show authorship. This version of the code is not usable. diff --git a/pyproject.toml b/pyproject.toml index 7e8caa6fe..5722067da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "landlab>=2.6.0", "matplotlib", "networkx", - "numpy", + "numpy<2", "opencv-python", "pandas", "psutil",