glitch3d 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +55 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/glitch3d +6 -0
- data/bin/setup +8 -0
- data/fixtures/cube.obj +18 -0
- data/fixtures/demo.png +0 -0
- data/fixtures/m4a1.obj +128741 -0
- data/fixtures/male_head.obj +22543 -0
- data/fixtures/skull.obj +34962 -0
- data/fixtures/textures/00.jpg +0 -0
- data/fixtures/textures/01.jpg +0 -0
- data/fixtures/textures/02.jpg +0 -0
- data/fixtures/textures/03.jpg +0 -0
- data/fixtures/textures/04.jpg +0 -0
- data/fixtures/textures/05.jpg +0 -0
- data/fixtures/textures/06.jpg +0 -0
- data/fixtures/textures/07.jpg +0 -0
- data/fixtures/textures/08.jpg +0 -0
- data/fixtures/textures/09.jpg +0 -0
- data/fixtures/textures/10.jpg +0 -0
- data/fixtures/textures/11.jpg +0 -0
- data/fixtures/textures/12.jpg +0 -0
- data/fixtures/textures/13.jpg +0 -0
- data/fixtures/textures/14.jpg +0 -0
- data/fixtures/textures/15.jpg +0 -0
- data/fixtures/textures/16.jpg +0 -0
- data/fixtures/textures/17.jpg +0 -0
- data/fixtures/textures/18.jpg +0 -0
- data/fixtures/textures/19.jpg +0 -0
- data/fixtures/textures/20.jpg +0 -0
- data/fixtures/textures/21.jpg +0 -0
- data/fixtures/textures/22.jpg +0 -0
- data/fixtures/textures/23.jpg +0 -0
- data/fixtures/textures/24.jpg +0 -0
- data/fixtures/textures/25.jpg +0 -0
- data/fixtures/textures/27.jpg +0 -0
- data/fixtures/textures/28.jpg +0 -0
- data/fixtures/textures/29.jpg +0 -0
- data/fixtures/textures/30.jpg +0 -0
- data/glitch3d.gemspec +34 -0
- data/lib/glitch3d/bpy/helpers.py +97 -0
- data/lib/glitch3d/bpy/rendering.py +159 -0
- data/lib/glitch3d/objects/face.rb +22 -0
- data/lib/glitch3d/objects/vertex.rb +55 -0
- data/lib/glitch3d/strategies/default.rb +17 -0
- data/lib/glitch3d/strategies/localized.rb +25 -0
- data/lib/glitch3d/version.rb +3 -0
- data/lib/glitch3d.rb +137 -0
- metadata +157 -0
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
| Binary file | 
    
        data/glitch3d.gemspec
    ADDED
    
    | @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 2 | 
            +
            lib = File.expand_path('../lib', __FILE__)
         | 
| 3 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 4 | 
            +
            require 'glitch3d/version'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Gem::Specification.new do |spec|
         | 
| 7 | 
            +
              spec.name          = "glitch3d"
         | 
| 8 | 
            +
              spec.version       = Glitch3d::VERSION
         | 
| 9 | 
            +
              spec.authors       = ["pskl"]
         | 
| 10 | 
            +
              spec.email         = ["hello@pascal.cc"]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              spec.summary       = %q{Alter 3D models and renders pictures.}
         | 
| 13 | 
            +
              spec.description   = %q{Glitch3D is a library designed to transform a 3D model randomly and render screenshots.}
         | 
| 14 | 
            +
              spec.homepage      = "http://pascal.cc"
         | 
| 15 | 
            +
              spec.license       = "MIT"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
         | 
| 18 | 
            +
              # to allow pushing to a single host or delete this section to allow pushing to any host.
         | 
| 19 | 
            +
              # if spec.respond_to?(:metadata)
         | 
| 20 | 
            +
              #   spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
         | 
| 21 | 
            +
              # else
         | 
| 22 | 
            +
              #   raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
         | 
| 23 | 
            +
              # end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
         | 
| 26 | 
            +
              spec.bindir        = "bin"
         | 
| 27 | 
            +
              spec.executables   = ["glitch3d"]
         | 
| 28 | 
            +
              spec.require_paths = ["lib"]
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              spec.add_development_dependency "bundler", "~> 1.12"
         | 
| 31 | 
            +
              spec.add_development_dependency "rake", "~> 10.0"
         | 
| 32 | 
            +
              spec.add_development_dependency "rspec", "~> 3.0"
         | 
| 33 | 
            +
              spec.add_development_dependency "byebug"
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,97 @@ | |
| 1 | 
            +
            # Helper methods
         | 
| 2 | 
            +
            def look_at(camera_object, point):
         | 
| 3 | 
            +
                location_camera = camera_object.matrix_world.to_translation()
         | 
| 4 | 
            +
                location_point = point.matrix_world.to_translation()
         | 
| 5 | 
            +
                direction = location_point - location_camera
         | 
| 6 | 
            +
                rot_quat = direction.to_track_quat('-Z', 'Y')
         | 
| 7 | 
            +
                camera_object.rotation_euler = rot_quat.to_euler()
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            def empty_materials():
         | 
| 10 | 
            +
                mats = bpy.data.materials
         | 
| 11 | 
            +
                for mat in mats.keys():
         | 
| 12 | 
            +
                    mats.remove(mats[mat])
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            def shoot(camera, model_object, filepath):
         | 
| 15 | 
            +
                look_at(camera, model_object)
         | 
| 16 | 
            +
                print('Camera now at location: ' + camera_location_string(camera) + ' / rotation: ' + camera_rotation_string(camera))
         | 
| 17 | 
            +
                bpy.context.scene.render.filepath = filepath
         | 
| 18 | 
            +
                bpy.ops.render.render(write_still=True)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            def output_name(index, model_path):
         | 
| 21 | 
            +
                return 'renders/' + os.path.splitext(model_path)[0].split('/')[1] + '_' + str(index) + '_' + str(datetime.date.today()) + '.png'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            def rotate(model_object, index):
         | 
| 24 | 
            +
                model_object.rotation_euler[2] = math.radians(index * (360.0 / shots_number))
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            def rand_color_value():
         | 
| 27 | 
            +
                return round(random.uniform(0.1, 1.0), 10)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            def rand_location():
         | 
| 30 | 
            +
                return (rand_location_value(), rand_location_value(), rand_location_value())
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            def rand_rotation():
         | 
| 33 | 
            +
                return (rand_rotation_value(), rand_rotation_value(), rand_rotation_value())
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            def rand_rotation_value():
         | 
| 36 | 
            +
                return round(random.uniform(0, 1), 10)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            def rand_location_value():
         | 
| 39 | 
            +
                return round(random.uniform(-8, 8), 10)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            def rand_color_vector():
         | 
| 42 | 
            +
                return (rand_color_value(), rand_color_value(), rand_color_value(), 1)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            def rand_scale():
         | 
| 45 | 
            +
                return round(random.uniform(0, 0.3), 10)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            def get_args():
         | 
| 48 | 
            +
              parser = argparse.ArgumentParser()
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              # get all script args
         | 
| 51 | 
            +
              _, all_arguments = parser.parse_known_args()
         | 
| 52 | 
            +
              double_dash_index = all_arguments.index('--')
         | 
| 53 | 
            +
              script_args = all_arguments[double_dash_index + 1: ]
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              # add parser rules
         | 
| 56 | 
            +
              parser.add_argument('-f', '--file', help="obj file to render")
         | 
| 57 | 
            +
              parser.add_argument('-n', '--shots-number', help="number of shots desired")
         | 
| 58 | 
            +
              parser.add_argument('-m', '--mode', help="quality mode: low | high")
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              parsed_script_args, _ = parser.parse_known_args(script_args)
         | 
| 61 | 
            +
              return parsed_script_args
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            def camera_rotation_string(camera):
         | 
| 64 | 
            +
                return str(int(camera.rotation_euler.x)) + ' ' + str(int(camera.rotation_euler.y)) + ' ' + str(int(camera.rotation_euler.z))
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            def camera_location_string(camera):
         | 
| 67 | 
            +
                return str(int(camera.location.x)) + ' ' + str(int(camera.location.y)) + ' ' + str(int(camera.location.z))
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            def assign_material(model_object, material):
         | 
| 70 | 
            +
                model_object.data.materials.append(material)
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            def assign_node_to_output(material, new_node):
         | 
| 73 | 
            +
                assert material.use_nodes == True
         | 
| 74 | 
            +
                output_node = material.node_tree.nodes['Material Output']
         | 
| 75 | 
            +
                material.node_tree.links.new(new_node.outputs[0], output_node.inputs['Surface'])
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            def create_cycles_material():
         | 
| 78 | 
            +
                material = bpy.data.materials.new('Object Material - ' + str(uuid.uuid1()))
         | 
| 79 | 
            +
                material.use_nodes = True
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                nodes = material.node_tree.nodes
         | 
| 82 | 
            +
                new_node = nodes.new(random.choice(SHADERS))
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                assign_node_to_output(material, new_node)
         | 
| 85 | 
            +
                return material
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            def assign_random_texture_to_material(material):
         | 
| 88 | 
            +
                assert material.use_nodes == True
         | 
| 89 | 
            +
                bsdf_node = material.node_tree.nodes['Diffuse BSDF']
         | 
| 90 | 
            +
                assign_node_to_output(material, bsdf_node)
         | 
| 91 | 
            +
                texture_node = material.node_tree.nodes.new('ShaderNodeTexture')
         | 
| 92 | 
            +
                material.node_tree.links.new(texture_node.outputs[0], bsdf_node.inputs[0])
         | 
| 93 | 
            +
                # code.interact(local=dict(globals(), **locals()))
         | 
| 94 | 
            +
                texture_path = os.path.expanduser('fixtures/textures/25.jpg')
         | 
| 95 | 
            +
                new_texture = bpy.data.textures.new('ColorTex', type = 'IMAGE')
         | 
| 96 | 
            +
                new_texture.image = bpy.data.images.load(texture_path)
         | 
| 97 | 
            +
                texture_node.texture = new_texture
         | 
| @@ -0,0 +1,159 @@ | |
| 1 | 
            +
            # Rendering script
         | 
| 2 | 
            +
            # Run by calling the blender executable with -b -P <script_name>
         | 
| 3 | 
            +
            # Disclaimer: I never did Python before so this is mostly hacks
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # process:
         | 
| 6 | 
            +
            # 1) Load model given as a parameter
         | 
| 7 | 
            +
            # 2) Create emitting surfaces to act as lamps
         | 
| 8 | 
            +
            # 3) Create camera
         | 
| 9 | 
            +
            # 4) Rotate model and shoot image at each step
         | 
| 10 | 
            +
            #
         | 
| 11 | 
            +
            # Use `code.interact(local=dict(globals(), **locals()))` to pry into the script
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            import bpy
         | 
| 14 | 
            +
            import os
         | 
| 15 | 
            +
            import argparse
         | 
| 16 | 
            +
            import datetime
         | 
| 17 | 
            +
            import bmesh
         | 
| 18 | 
            +
            import random
         | 
| 19 | 
            +
            import code
         | 
| 20 | 
            +
            import math
         | 
| 21 | 
            +
            import mathutils
         | 
| 22 | 
            +
            import random
         | 
| 23 | 
            +
            import uuid
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            exec(open("lib/glitch3d/bpy/helpers.py").read())
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            # Arguments parsing
         | 
| 28 | 
            +
            args = get_args()
         | 
| 29 | 
            +
            file = args.file
         | 
| 30 | 
            +
            mode = args.mode
         | 
| 31 | 
            +
            shots_number = int(args.shots_number)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            context = bpy.context
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            REFLECTOR_SCALE = 5
         | 
| 36 | 
            +
            REFLECTOR_STRENGTH = 12
         | 
| 37 | 
            +
            REFLECTOR_LOCATION_PADDING = 10
         | 
| 38 | 
            +
            PROPS_NUMBER = 100
         | 
| 39 | 
            +
            SHADERS = ['ShaderNodeBsdfGlossy', 'ShaderNodeBsdfDiffuse', 'ShaderNodeBsdfVelvet']
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            # Scene
         | 
| 42 | 
            +
            new_scene = bpy.data.scenes.new("Automated Render Scene")
         | 
| 43 | 
            +
            bpy.ops.scene.delete() # Delete old scene
         | 
| 44 | 
            +
            context.screen.scene = new_scene # selects the new scene as the current one
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            # Render settings
         | 
| 47 | 
            +
            context.scene.render.resolution_x = 1920
         | 
| 48 | 
            +
            context.scene.render.resolution_y = 1080
         | 
| 49 | 
            +
            context.scene.render.engine = 'CYCLES'
         | 
| 50 | 
            +
            context.scene.render.resolution_percentage = 25
         | 
| 51 | 
            +
            # bpy.context.scene.cycles.device = 'GPU'
         | 
| 52 | 
            +
            context.scene.render.image_settings.compression = 0
         | 
| 53 | 
            +
            context.scene.cycles.samples = 25
         | 
| 54 | 
            +
            context.scene.render.image_settings.color_mode ='RGBA'
         | 
| 55 | 
            +
            context.scene.render.image_settings.file_format='PNG'
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            if mode == 'high':
         | 
| 58 | 
            +
                context.scene.render.image_settings.compression = 90
         | 
| 59 | 
            +
                context.scene.cycles.samples = 500
         | 
| 60 | 
            +
                context.scene.render.resolution_percentage = 100
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            # Load model
         | 
| 63 | 
            +
            model_path = os.path.join(file)
         | 
| 64 | 
            +
            bpy.ops.import_scene.obj(filepath = model_path, use_edges=True)
         | 
| 65 | 
            +
            model_object = bpy.data.objects['Glitch3D']
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            # Use center of mass to center object
         | 
| 68 | 
            +
            model_object.select = True
         | 
| 69 | 
            +
            bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
         | 
| 70 | 
            +
            model_object.location = (0, 0, 0)
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            # --------------
         | 
| 73 | 
            +
            # Create camera
         | 
| 74 | 
            +
            # --------------
         | 
| 75 | 
            +
            camera_data = bpy.data.cameras.new('Render Camera')
         | 
| 76 | 
            +
            bpy.data.objects.new('Render Camera', object_data=camera_data)
         | 
| 77 | 
            +
            camera_object = bpy.data.objects['Render Camera']
         | 
| 78 | 
            +
            new_scene.objects.link(camera_object)
         | 
| 79 | 
            +
            camera_object.location = (8, 8, 0)
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            # Add reflectors
         | 
| 82 | 
            +
            bpy.ops.mesh.primitive_plane_add(location=(0,8 + REFLECTOR_LOCATION_PADDING, 0))
         | 
| 83 | 
            +
            bpy.ops.mesh.primitive_plane_add(location=(8 + REFLECTOR_LOCATION_PADDING,0,0))
         | 
| 84 | 
            +
             | 
| 85 | 
            +
            plane1 = bpy.data.objects['Plane']
         | 
| 86 | 
            +
            plane2 = bpy.data.objects['Plane.001']
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            # Adjust camera
         | 
| 89 | 
            +
            context.scene.camera = camera_object
         | 
| 90 | 
            +
            look_at(camera_object, model_object)
         | 
| 91 | 
            +
            assign_material(model_object, create_cycles_material())
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            for index, plane in enumerate([plane1, plane2]):
         | 
| 94 | 
            +
                plane.scale = (REFLECTOR_SCALE, REFLECTOR_SCALE, REFLECTOR_SCALE)
         | 
| 95 | 
            +
                plane.rotation_euler.x += math.radians(90)
         | 
| 96 | 
            +
                emissive_material = bpy.data.materials.new('Emissive Material #' + str(index))
         | 
| 97 | 
            +
                emissive_material.use_nodes = True
         | 
| 98 | 
            +
                emission_node = emissive_material.node_tree.nodes.new('ShaderNodeEmission')
         | 
| 99 | 
            +
                # Set color
         | 
| 100 | 
            +
                emission_node.inputs[0].default_value = rand_color_vector()
         | 
| 101 | 
            +
                # Set strength
         | 
| 102 | 
            +
                emission_node.inputs[1].default_value = REFLECTOR_STRENGTH
         | 
| 103 | 
            +
                assign_node_to_output(emissive_material, emission_node)
         | 
| 104 | 
            +
                assign_material(plane, emissive_material)
         | 
| 105 | 
            +
             | 
| 106 | 
            +
            # Tilt one of the reflectors
         | 
| 107 | 
            +
            plane2.rotation_euler.z += math.radians(90)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            # Add other elements
         | 
| 110 | 
            +
            for index in range(0, int(PROPS_NUMBER)):
         | 
| 111 | 
            +
                bpy.ops.mesh.primitive_cube_add(location=rand_location(),radius=rand_scale(), rotation=rand_rotation())
         | 
| 112 | 
            +
                if index == 0:
         | 
| 113 | 
            +
                    object_name = 'Cube'
         | 
| 114 | 
            +
                elif index > 9:
         | 
| 115 | 
            +
                    object_name = 'Cube.0' + str(index)
         | 
| 116 | 
            +
                elif index > 99:
         | 
| 117 | 
            +
                    object_name = 'Cube.' + str(index)
         | 
| 118 | 
            +
                else:
         | 
| 119 | 
            +
                    object_name = 'Cube.00' + str(index)
         | 
| 120 | 
            +
                object = bpy.data.objects[object_name]
         | 
| 121 | 
            +
                new_material = create_cycles_material()
         | 
| 122 | 
            +
                assign_random_texture_to_material(new_material)
         | 
| 123 | 
            +
                assign_material(object, new_material)
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            for index in range(0, int(PROPS_NUMBER)):
         | 
| 126 | 
            +
                bpy.ops.mesh.primitive_circle_add(location=rand_location(),radius=rand_scale(), rotation=rand_rotation())
         | 
| 127 | 
            +
                if index == 0:
         | 
| 128 | 
            +
                    object_name = 'Circle'
         | 
| 129 | 
            +
                elif index > 9:
         | 
| 130 | 
            +
                    object_name = 'Circle.0' + str(index)
         | 
| 131 | 
            +
                elif index > 99:
         | 
| 132 | 
            +
                    object_name = 'Circle.' + str(index)
         | 
| 133 | 
            +
                else:
         | 
| 134 | 
            +
                    object_name = 'Circle.00' + str(index)
         | 
| 135 | 
            +
                object = bpy.data.objects[object_name]
         | 
| 136 | 
            +
                new_material = create_cycles_material()
         | 
| 137 | 
            +
                assign_random_texture_to_material(new_material)
         | 
| 138 | 
            +
                assign_material(object, new_material)
         | 
| 139 | 
            +
             | 
| 140 | 
            +
            # Add background to world
         | 
| 141 | 
            +
            world = bpy.data.worlds[0]
         | 
| 142 | 
            +
            world.use_nodes = True
         | 
| 143 | 
            +
            world_node_tree = world.node_tree
         | 
| 144 | 
            +
            # code.interact(local=dict(globals(), **locals()))
         | 
| 145 | 
            +
            gradient_node = world_node_tree.nodes.new(type="ShaderNodeTexGradient")
         | 
| 146 | 
            +
            background_node = world_node_tree.nodes['Background']
         | 
| 147 | 
            +
            world_node_tree.links.new(gradient_node.outputs['Color'], background_node.inputs['Color'])
         | 
| 148 | 
            +
            gradient_node.gradient_type = 'EASING'
         | 
| 149 | 
            +
             | 
| 150 | 
            +
            # ------
         | 
| 151 | 
            +
            # Shoot
         | 
| 152 | 
            +
            # ------
         | 
| 153 | 
            +
            print('Rendering images with resolution: ' + str(context.scene.render.resolution_x) + ' x ' + str(context.scene.render.resolution_y))
         | 
| 154 | 
            +
            for index in range(0, int(shots_number)):
         | 
| 155 | 
            +
                print("-------------------------- " + str(index) + " --------------------------")
         | 
| 156 | 
            +
                rotate(model_object, index)
         | 
| 157 | 
            +
                shoot(camera_object, model_object, output_name(index, model_path))
         | 
| 158 | 
            +
             | 
| 159 | 
            +
            print('FINISHED ¯\_(ツ)_/¯')
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            class Face
         | 
| 2 | 
            +
              attr_accessor :v1, :v2, :v3
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def initialize(v1, v2, v3)
         | 
| 5 | 
            +
                @v1 = v1
         | 
| 6 | 
            +
                @v2 = v2
         | 
| 7 | 
            +
                @v3 = v3
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def to_s
         | 
| 11 | 
            +
                return nil unless !v1.nil? && !v2.nil? && !v3.nil?
         | 
| 12 | 
            +
                "f #{v1.index} #{v2.index} #{v3.index}"
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def rand_attr
         | 
| 16 | 
            +
                [:v1, :v2, :v3].sample
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def fuck(new_vertex)
         | 
| 20 | 
            +
                send("#{rand_attr}=", new_vertex)
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            class Vertex
         | 
| 2 | 
            +
              attr_accessor :x, :y, :z, :index
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def initialize(x, y, z, index)
         | 
| 5 | 
            +
                @x = x
         | 
| 6 | 
            +
                @y = y
         | 
| 7 | 
            +
                @z = z
         | 
| 8 | 
            +
                @index = index
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def to_s
         | 
| 12 | 
            +
                "v #{x} #{y} #{z}"
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def rand_attr
         | 
| 16 | 
            +
                [:x, :y, :z].sample
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def rescale(offset)
         | 
| 20 | 
            +
                [:x, :y, :z].each do |attr|
         | 
| 21 | 
            +
                  value = send(attr)
         | 
| 22 | 
            +
                  send("#{attr}=", value * 0.8)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              def fuck
         | 
| 27 | 
            +
                attr = rand_attr
         | 
| 28 | 
            +
                send("#{attr}=", send(attr) + Glitch3d.rand_vertex_glitch_offset)
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def max
         | 
| 32 | 
            +
                [@x.abs, @y.abs].max.round
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def self.boundaries(vertices_list)
         | 
| 36 | 
            +
                [
         | 
| 37 | 
            +
                  [vertices_list.max_by(&:x).x.ceil, vertices_list.min_by(&:x).x.round],
         | 
| 38 | 
            +
                  [vertices_list.max_by(&:y).y.ceil, vertices_list.min_by(&:y).y.round],
         | 
| 39 | 
            +
                  [vertices_list.max_by(&:z).z.ceil, vertices_list.min_by(&:z).z.round]
         | 
| 40 | 
            +
                ]
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def self.rescale(vertices, offset)
         | 
| 44 | 
            +
                vertices.each do |v|
         | 
| 45 | 
            +
                  v.rescale(offset)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              # Pass functions like :negative? or :positive?
         | 
| 50 | 
            +
              def self.subset(x:, y:, z:, vertex_list:)
         | 
| 51 | 
            +
                vertex_list.select do |vertex|
         | 
| 52 | 
            +
                  vertex.x.send(x) && vertex.y.send(y) && vertex.y.send(z)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module Glitch3d
         | 
| 2 | 
            +
              module Default
         | 
| 3 | 
            +
                def alter_vertices(vertices_objects_array)
         | 
| 4 | 
            +
                  (VERTEX_GLITCH_ITERATION_RATIO * vertices_objects_array.size).to_i.times do |_|
         | 
| 5 | 
            +
                    random_element(vertices_objects_array).fuck
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
                  vertices_objects_array
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def alter_faces(faces_objects_array, vertex_objects_array)
         | 
| 11 | 
            +
                  (FACE_GLITCH_ITERATION_RATIO * faces_objects_array.count).to_i.times do |_|
         | 
| 12 | 
            +
                    random_element(faces_objects_array).fuck(random_element(vertex_objects_array))
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                  faces_objects_array
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            module Glitch3d
         | 
| 2 | 
            +
              module Localized
         | 
| 3 | 
            +
                def alter_vertices(vertices_objects_array)
         | 
| 4 | 
            +
                  (VERTEX_GLITCH_ITERATION_RATIO * vertices_objects_array.size).to_i.times do |_|
         | 
| 5 | 
            +
                    random_element(target(vertices_objects_array)).fuck
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
                  vertices_objects_array
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def alter_faces(faces_objects_array, vertices_objects_array)
         | 
| 11 | 
            +
                  (FACE_GLITCH_ITERATION_RATIO * faces_objects_array.count).to_i.times do |_|
         | 
| 12 | 
            +
                    random_element(faces_objects_array).fuck(random_element(target(vertices_objects_array)))
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                  faces_objects_array
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def selected_area(vertices_objects_array)
         | 
| 18 | 
            +
                  Vertex.subset(x: :negative?, y: :positive?, z: :zero?, vertex_list: vertices_objects_array)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def target(vertices_objects_array)
         | 
| 22 | 
            +
                  @target ||= selected_area(vertices_objects_array)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
    
        data/lib/glitch3d.rb
    ADDED
    
    | @@ -0,0 +1,137 @@ | |
| 1 | 
            +
            require "glitch3d/version"
         | 
| 2 | 
            +
            require "glitch3d/objects/vertex"
         | 
| 3 | 
            +
            require "glitch3d/objects/face"
         | 
| 4 | 
            +
            require "glitch3d/strategies/default"
         | 
| 5 | 
            +
            require "glitch3d/strategies/localized"
         | 
| 6 | 
            +
            require "pry"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module Glitch3d
         | 
| 9 | 
            +
              VERTEX_GLITCH_ITERATION_RATIO = 0.001
         | 
| 10 | 
            +
              VERTEX_GLITCH_OFFSET = 1
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              FACE_GLITCH_ITERATION_RATIO = 0.0
         | 
| 13 | 
            +
              FACE_GLITCH_OFFSET = 0.5
         | 
| 14 | 
            +
              BOUNDARY_LIMIT = 4 # Contain model within 2x2x2 cube
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              BLENDER_EXECUTABLE_PATH = ENV['BLENDER_EXECUTABLE_PATH'].freeze
         | 
| 17 | 
            +
              RENDERING_SCRIPT_PATH = "lib/glitch3d/bpy/rendering.py".freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def process_model(source_file)
         | 
| 20 | 
            +
                raise 'Set Blender executable path in your env variables before using glitch3d' unless BLENDER_EXECUTABLE_PATH.present?
         | 
| 21 | 
            +
                args = Hash[ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/)]
         | 
| 22 | 
            +
                self.class.include infer_strategy(args["mode"] || 'default')
         | 
| 23 | 
            +
                @quality = args["quality"] || 'low'
         | 
| 24 | 
            +
                source_file = source_file
         | 
| 25 | 
            +
                base_file_name = source_file.gsub(/.obj/, '')
         | 
| 26 | 
            +
                target_file = base_file_name + '_glitched.obj'
         | 
| 27 | 
            +
                boundaries = create_glitched_file(glitch(read_source(source_file)), target_file)
         | 
| 28 | 
            +
                render(target_file, boundaries, args["shots-number"] || 6) unless args["no-render"]
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def infer_strategy(mode)
         | 
| 32 | 
            +
                return Glitch3d::Default if mode.nil?
         | 
| 33 | 
            +
                {
         | 
| 34 | 
            +
                  default: Glitch3d::Default,
         | 
| 35 | 
            +
                  localized: Glitch3d::Localized
         | 
| 36 | 
            +
                }[mode.to_sym]
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              class << self
         | 
| 40 | 
            +
                def rand_vertex_glitch_offset
         | 
| 41 | 
            +
                  rand(-VERTEX_GLITCH_OFFSET..VERTEX_GLITCH_OFFSET)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def read_source(path)
         | 
| 48 | 
            +
                File.open(path, 'r') do |f|
         | 
| 49 | 
            +
                  source_file_content = f.readlines
         | 
| 50 | 
            +
                  vertices_list = build_vertices(source_file_content.select { |s| s[0..1] == 'v ' })
         | 
| 51 | 
            +
                  faces_list = build_faces(source_file_content.select { |s| s[0..1] == 'f ' }, vertices_list)
         | 
| 52 | 
            +
                  {
         | 
| 53 | 
            +
                    vertices: vertices_list,
         | 
| 54 | 
            +
                    faces: faces_list
         | 
| 55 | 
            +
                  }
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              def build_vertices(vertices_string_array)
         | 
| 60 | 
            +
                vertices_list = []
         | 
| 61 | 
            +
                vertices_string_array.each_with_index.map do |sv, i|
         | 
| 62 | 
            +
                  v = sv.split(' ')
         | 
| 63 | 
            +
                  vertices_list << Vertex.new(v[1].to_f, v[2].to_f, v[3].to_f, i)
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
                vertices_list
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              def build_faces(faces_string_array, vertices_list)
         | 
| 69 | 
            +
                faces_list = []
         | 
| 70 | 
            +
                faces_string_array.map do |sf|
         | 
| 71 | 
            +
                  f = sf.split(' ')
         | 
| 72 | 
            +
                  next if f.length <= 3
         | 
| 73 | 
            +
                  faces_list << Face.new(
         | 
| 74 | 
            +
                                  vertices_list[f[1].to_i],
         | 
| 75 | 
            +
                                  vertices_list[f[2].to_i],
         | 
| 76 | 
            +
                                  vertices_list[f[3].to_i]
         | 
| 77 | 
            +
                                )
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
                faces_list
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
              def glitch(file_hash_content)
         | 
| 83 | 
            +
                {
         | 
| 84 | 
            +
                  vertices: alter_vertices(file_hash_content[:vertices]),
         | 
| 85 | 
            +
                  faces: alter_faces(file_hash_content[:faces], file_hash_content[:vertices])
         | 
| 86 | 
            +
                }
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
              def random_element(array)
         | 
| 90 | 
            +
                array[rand(0..array.size - 1)]
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              def create_glitched_file(content_hash, target_file)
         | 
| 94 | 
            +
                boundaries = Vertex.boundaries(content_hash[:vertices])
         | 
| 95 | 
            +
                puts boundaries.to_s
         | 
| 96 | 
            +
                while rescale_needed?(boundaries)
         | 
| 97 | 
            +
                  content_hash[:vertices] = Vertex.rescale(content_hash[:vertices], (boundaries.flatten.map(&:abs).max.abs - BOUNDARY_LIMIT).abs)
         | 
| 98 | 
            +
                  boundaries = Vertex.boundaries(content_hash[:vertices])
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
                boundaries = Vertex.boundaries(content_hash[:vertices])
         | 
| 101 | 
            +
                puts boundaries.to_s
         | 
| 102 | 
            +
                File.open(target_file, 'w') do |f|
         | 
| 103 | 
            +
                  f.puts '# Data corrupted with glitch3D script'
         | 
| 104 | 
            +
                  f.puts '# Boundaries: ' +  boundaries.to_s
         | 
| 105 | 
            +
                  f.puts ''
         | 
| 106 | 
            +
                  f.puts 'g Glitch3D'
         | 
| 107 | 
            +
                  f.puts ''
         | 
| 108 | 
            +
                  f.puts content_hash[:vertices].map(&:to_s)
         | 
| 109 | 
            +
                  f.puts ''
         | 
| 110 | 
            +
                  f.puts content_hash[:faces].map(&:to_s).compact
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
                boundaries
         | 
| 113 | 
            +
              end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              def rescale_needed?(boundaries)
         | 
| 116 | 
            +
                boundaries.flatten.map(&:abs).max.abs > BOUNDARY_LIMIT
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
              def render(file_path, boundaries, shots_number)
         | 
| 120 | 
            +
                args = [
         | 
| 121 | 
            +
                  BLENDER_EXECUTABLE_PATH,
         | 
| 122 | 
            +
                  '-b',
         | 
| 123 | 
            +
                  '-P',
         | 
| 124 | 
            +
                  RENDERING_SCRIPT_PATH,
         | 
| 125 | 
            +
                  '--',
         | 
| 126 | 
            +
                  '-f',
         | 
| 127 | 
            +
                  file_path,
         | 
| 128 | 
            +
                  '-n',
         | 
| 129 | 
            +
                  shots_number.to_s,
         | 
| 130 | 
            +
                  '-m',
         | 
| 131 | 
            +
                  @quality
         | 
| 132 | 
            +
                ]
         | 
| 133 | 
            +
                unless system(*args)
         | 
| 134 | 
            +
                  fail 'Make sure Blender is correctly installed'
         | 
| 135 | 
            +
                end
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
            end
         |