Created
November 29, 2021 06:07
-
-
Save CGArtPython/2616614d00583e121c0bbddb10cf3e52 to your computer and use it in GitHub Desktop.
The code for this art: https://www.artstation.com/artwork/48wX6L
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
The code for this art project: | |
https://www.artstation.com/artwork/48wX6L | |
Tested using: Blender 2.93 | |
Author: Viktor Stepanov | |
License info: https://choosealicense.com/licenses/mit/ | |
============================================================================= | |
MIT License | |
Copyright (c) 2021 Viktor Stepanov | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
============================================================================= | |
""" | |
import contextlib | |
import random | |
import time | |
import math | |
import bpy | |
import bmesh | |
import mathutils | |
import addon_utils | |
################################################################ | |
# helpder functions BEGIN | |
################################################################ | |
def enable_extra_curves(): | |
""" | |
Add Curve Extra Objects | |
https://docs.blender.org/manual/en/2.93/addons/add_curve/extra_objects.html | |
""" | |
loaded_default, loaded_state = addon_utils.check("add_curve_extra_objects") | |
if not loaded_state: | |
addon_utils.enable("add_curve_extra_objects") | |
def delete_all_objects(): | |
""" | |
Removing all of the objects from the scene | |
""" | |
if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": | |
bpy.ops.object.editmode_toggle() | |
for obj in bpy.data.objects: | |
obj.hide_set(False) | |
obj.hide_select = False | |
obj.hide_viewport = False | |
bpy.ops.object.select_all(action="SELECT") | |
bpy.ops.object.delete() | |
collection_names = [col.name for col in bpy.data.collections] | |
for name in collection_names: | |
bpy.data.collections.remove(bpy.data.collections[name]) | |
action_names = [action.name for action in bpy.data.actions] | |
for name in action_names: | |
bpy.data.actions.remove(bpy.data.actions[name]) | |
camera_names = [camera.name for camera in bpy.data.cameras] | |
for name in camera_names: | |
bpy.data.cameras.remove(bpy.data.cameras[name]) | |
curve_names = [curve.name for curve in bpy.data.curves] | |
for name in curve_names: | |
bpy.data.curves.remove(bpy.data.curves[name]) | |
# free the rest of the data materials, particles, textures, meshes, geo nodes | |
bpy.ops.outliner.orphans_purge( | |
do_local_ids=True, | |
do_linked_ids=True, | |
do_recursive=True, | |
) | |
def get_random_pallet_color(context): | |
return random.choice(context["colors"]) | |
def active_obj(): | |
""" | |
returns the active object | |
""" | |
return bpy.context.active_object | |
def time_seed(): | |
""" | |
Sets the random seed based on the time | |
and copies the seed into the clipboard | |
""" | |
seed = time.time() | |
print(f"seed: {seed}") | |
random.seed(seed) | |
bpy.context.window_manager.clipboard = str(seed) | |
return seed | |
def add_ctrl_empty(name=None): | |
bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") | |
empty_ctrl = bpy.context.active_object | |
if name: | |
empty_ctrl.name = name | |
else: | |
empty_ctrl.name = "empty.cntrl" | |
return empty_ctrl | |
def apply_mat(material): | |
obj = bpy.context.active_object | |
obj.data.materials.append(material) | |
def make_active(obj): | |
bpy.ops.object.select_all(action="DESELECT") | |
obj.select_set(True) | |
bpy.context.view_layer.objects.active = obj | |
def track_empty(cam): | |
bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD", location=(0, 0, 0)) | |
empty = active_obj() | |
empty.name = "tracker-target" | |
make_active(cam) | |
bpy.ops.object.constraint_add(type="TRACK_TO") | |
bpy.context.object.constraints["Track To"].target = empty | |
return empty | |
def setup_camera(loc, rot): | |
bpy.ops.object.camera_add( | |
enter_editmode=False, align="VIEW", location=loc, rotation=rot | |
) | |
cam = bpy.context.active_object | |
bpy.context.scene.camera = cam | |
bpy.context.object.data.lens = 70 | |
bpy.context.object.data.passepartout_alpha = 0.9 | |
empty = track_empty(cam) | |
return empty | |
def set_1k_square_render_res(): | |
""" | |
Set the resolution of the rendered image to 1080 by 1080 | |
""" | |
bpy.context.scene.render.resolution_x = 1080 | |
bpy.context.scene.render.resolution_y = 1080 | |
def set_scene_props(fps, loop_sec): | |
""" | |
Set scene properties | |
""" | |
frame_count = fps * loop_sec | |
scene = bpy.context.scene | |
scene.frame_end = frame_count | |
world = bpy.data.worlds["World"] | |
if "Background" in world.node_tree.nodes: | |
world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) | |
scene.render.fps = fps | |
scene.frame_current = 1 | |
scene.frame_start = 1 | |
bpy.context.preferences.edit.use_negative_frames = True | |
scene.eevee.use_bloom = True | |
scene.eevee.bloom_intensity = 0.005 | |
scene.eevee.gtao_distance = 4 | |
scene.eevee.gtao_factor = 5 | |
scene.eevee.use_gtao = True | |
scene.eevee.use_ssr = True | |
scene.eevee.use_ssr_halfres = False | |
scene.eevee.ssr_quality = 1 | |
scene.eevee.taa_render_samples = 64 | |
scene.view_settings.look = "Very High Contrast" | |
set_1k_square_render_res() | |
def create_collection(col_name): | |
bpy.ops.object.select_all(action="DESELECT") | |
bpy.ops.collection.create(name=col_name) | |
collection = bpy.data.collections[col_name] | |
bpy.context.scene.collection.children.link(collection) | |
return collection | |
@contextlib.contextmanager | |
def editmode(): | |
# enter editmode | |
bpy.ops.object.editmode_toggle() | |
yield # return out of the function in editmode | |
# exit editmode | |
bpy.ops.object.editmode_toggle() | |
def get_vert_coordinates_list(obj): | |
coordinates_list = [] | |
with editmode(): | |
bm = bmesh.from_edit_mesh(obj.data) | |
bm.verts.ensure_lookup_table() | |
for vert in bm.verts: | |
vector = mathutils.Vector((vert.co.x, vert.co.y, vert.co.z)) | |
coordinates_list.append(vector) | |
return coordinates_list | |
def make_ramp_from_colors(colors, color_ramp_node): | |
""" | |
Creates new sliders on a Color Ramp Node and | |
applies the list of colors on each slider | |
""" | |
color_count = len(colors) | |
step = 1 / color_count | |
cur_pos = step | |
# -2 is for the two sliders that are present on the ramp | |
for _ in range(color_count - 2): | |
color_ramp_node.elements.new(cur_pos) | |
cur_pos += step | |
for i, color in enumerate(colors): | |
color_ramp_node.elements[i].color = color | |
def get_color_palette(): | |
# https://www.colourlovers.com/palette/2943292 | |
# palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] | |
palette = [ | |
[0.83984375, 0.8046875, 0.63671875, 0.99609375], | |
[0.5625, 0.46875, 0.1484375, 0.99609375], | |
[0.640625, 0.40234375, 0.09765625, 0.99609375], | |
[0.8046875, 0.24609375, 0.0546875, 0.99609375], | |
[0.1015625, 0.046875, 0.27734375, 0.99609375], | |
] | |
return palette | |
def apply_location(): | |
bpy.ops.object.transform_apply(location=True) | |
def add_lights(): | |
""" | |
To keep the code simple I have decided not to include the HDRI downloading and applying code. | |
I plan to make a tutorial video explaining how this can be done. | |
For what it's worth I used this HDRI: https://hdrihaven.com/hdri/?h=dresden_station_night | |
Please consider supporting hdrihaven.com @ https://www.patreon.com/polyhaven/overview | |
""" | |
# url = "https://dl.polyhaven.com/file/ph-assets/HDRIs/hdr/2k/dresden_station_night_2k.hdr" | |
# apply_hdri(url) | |
pass | |
################################################################ | |
# helpder functions END | |
################################################################ | |
def get_coordinates_list(radius, index): | |
vertices = 512 * radius | |
bpy.ops.mesh.primitive_circle_add( | |
vertices=vertices, | |
radius=radius, | |
enter_editmode=False, | |
location=(0, 0, 0.1 * index), | |
) | |
apply_location() | |
obj = active_obj() | |
bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN") | |
coordinates_list = get_vert_coordinates_list(obj) | |
bpy.data.objects.remove(bpy.data.objects[obj.name]) | |
return coordinates_list | |
def gen_perlin_curve(context, random_location, radius, index, collection_name): | |
coordinates_list = get_coordinates_list(radius, index) | |
non_deform_coords = coordinates_list | |
deform_coords = [] | |
current_z = 0.1 * index | |
curve_origin = mathutils.Vector((0, 0, current_z)) | |
for coord in non_deform_coords: | |
new_location = random_location + coord | |
noise_value = mathutils.noise.noise(new_location) | |
noise_value = noise_value / 2 | |
deform_vector = (coord - curve_origin) * noise_value | |
deform_coord = coord + deform_vector | |
deform_coords.append(deform_coord) | |
curve_obj = gen_curve_form_coords(index, deform_coords, collection_name) | |
make_active(curve_obj) | |
bpy.context.object.data.bevel_mode = "OBJECT" | |
bpy.context.object.data.bevel_object = context["bevel_obj"] | |
curve_obj.data.materials.append(context["material"]) | |
bpy.ops.object.shade_flat() | |
shape_key = add_shape_key(curve_obj, non_deform_coords) | |
return shape_key | |
def animate_curve(context, shape_key, start_frame): | |
start_value = 0 | |
mid_value = 1 | |
loop_len = context["frame_count"] - context["curve_count"] | |
shape_key.value = start_value | |
shape_key.keyframe_insert("value", frame=start_frame) | |
current_frame = start_frame + (loop_len) / 2 | |
shape_key.value = mid_value | |
shape_key.keyframe_insert("value", frame=current_frame) | |
current_frame = start_frame + loop_len | |
shape_key.value = start_value | |
shape_key.keyframe_insert("value", frame=current_frame) | |
frame_step = 1 | |
start_frame += frame_step | |
return start_frame | |
def gen_curve_form_coords(index, coords, collection_name): | |
curve_name = f"curve_{index}" | |
curve_data = bpy.data.curves.new(curve_name, type="CURVE") | |
poly_spline_data = curve_data.splines.new("POLY") | |
curve_obj = bpy.data.objects.new(curve_name, curve_data) | |
collection = bpy.data.collections[collection_name] | |
collection.objects.link(curve_obj) | |
point_count = len(coords) - 1 | |
poly_spline_data.points.add(point_count) | |
poly_spline_data.use_cyclic_u = True # make it loop | |
weight = 1 | |
for i, coord in enumerate(coords): | |
poly_spline_data.points[i].co = (coord.x, coord.y, coord.z, weight) | |
return curve_obj | |
def add_shape_key(curve_obj, non_deform_coords): | |
curve_obj.shape_key_add(name="Basis") | |
shape_key = curve_obj.shape_key_add(name="Deform") | |
for i, coord in enumerate(non_deform_coords): | |
shape_key.data[i].co = coord | |
shape_key.value = 1 | |
return shape_key | |
def create_bevel_object(): | |
bpy.ops.curve.simple( | |
Simple_Type="Rectangle", | |
edit_mode=False, | |
) | |
bpy.context.object.scale.x = 0.26 | |
bpy.context.object.scale.y = 0.05 | |
bpy.context.object.name = "bevel_object" | |
return active_obj() | |
def setup_material(context): | |
material_ctrl = add_ctrl_empty() | |
material, nodes = gen_base_material() | |
nodes["Texture Coordinate"].object = material_ctrl | |
nodes["Mapping"].inputs[3].default_value[2] = 0.1 | |
make_ramp_from_colors(context["colors"], nodes["ColorRamp"].color_ramp) | |
return material | |
def gen_centerpiece(context): | |
context["material"] = setup_material(context) | |
context["bevel_obj"] = create_bevel_object() | |
random_location = mathutils.Vector( | |
(random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100)) | |
) | |
collection_name = f"perlin_curves" | |
create_collection(collection_name) | |
count = 100 | |
context["curve_count"] = count | |
radius = 1.1 | |
start_frame = 1 | |
for i in range(count): | |
shape_key = gen_perlin_curve( | |
context, random_location, radius, i, collection_name | |
) | |
start_frame = animate_curve(context, shape_key, start_frame) | |
def gen_scene(context): | |
gen_centerpiece(context) | |
add_lights() | |
def gen_base_material(): | |
material = bpy.data.materials.new(name="base_material") | |
material.use_nodes = True | |
mat_output = None | |
to_rm = [] | |
for node in material.node_tree.nodes: | |
if node.type == "OUTPUT_MATERIAL": | |
mat_output = node | |
continue | |
to_rm.append(node) | |
for node in to_rm: | |
print("Removing", node.name) | |
material.node_tree.nodes.remove(node) | |
node_dict = dict() | |
mat_output.location = mathutils.Vector((300.0, 300.0)) | |
bsdf_principled_node = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") | |
bsdf_principled_node.location = mathutils.Vector((10.0, 300.0)) | |
bsdf_principled_node.name = "Principled BSDF" | |
# bsdf_principled_node.label = "" | |
node_dict["Principled BSDF"] = bsdf_principled_node | |
color_ramp_node = material.node_tree.nodes.new(type="ShaderNodeValToRGB") | |
color_ramp_node.location = mathutils.Vector((-533.505, 168.539)) | |
color_ramp_node.name = "ColorRamp" | |
# color_ramp_node.label = "" | |
node_dict["ColorRamp"] = color_ramp_node | |
texture_coordinate_node = material.node_tree.nodes.new(type="ShaderNodeTexCoord") | |
texture_coordinate_node.location = mathutils.Vector((-1397.75, 89.254)) | |
texture_coordinate_node.name = "Texture Coordinate" | |
# texture_coordinate_node.label = "" | |
node_dict["Texture Coordinate"] = texture_coordinate_node | |
mapping_node = material.node_tree.nodes.new(type="ShaderNodeMapping") | |
mapping_node.location = mathutils.Vector((-1197.749, 106.254)) | |
mapping_node.name = "Mapping" | |
# mapping_node.label = "" | |
node_dict["Mapping"] = mapping_node | |
mapping_node.inputs["Rotation"].default_value = mathutils.Euler( | |
(0.0, math.radians(90), 0.0) | |
) | |
gradient_texture_node = material.node_tree.nodes.new(type="ShaderNodeTexGradient") | |
gradient_texture_node.location = mathutils.Vector((-877.749, 133.323)) | |
gradient_texture_node.name = "Gradient Texture" | |
# gradient_texture_node.label = "" | |
node_dict["Gradient Texture"] = gradient_texture_node | |
# links begin | |
from_node = material.node_tree.nodes.get("Principled BSDF") | |
to_node = material.node_tree.nodes.get("Material Output") | |
material.node_tree.links.new(from_node.outputs["BSDF"], to_node.inputs["Surface"]) | |
from_node = material.node_tree.nodes.get("ColorRamp") | |
to_node = material.node_tree.nodes.get("Principled BSDF") | |
material.node_tree.links.new( | |
from_node.outputs["Color"], to_node.inputs["Base Color"] | |
) | |
from_node = material.node_tree.nodes.get("Mapping") | |
to_node = material.node_tree.nodes.get("Gradient Texture") | |
material.node_tree.links.new(from_node.outputs["Vector"], to_node.inputs["Vector"]) | |
from_node = material.node_tree.nodes.get("Texture Coordinate") | |
to_node = material.node_tree.nodes.get("Mapping") | |
material.node_tree.links.new(from_node.outputs["Object"], to_node.inputs["Vector"]) | |
from_node = material.node_tree.nodes.get("Gradient Texture") | |
to_node = material.node_tree.nodes.get("ColorRamp") | |
material.node_tree.links.new(from_node.outputs["Color"], to_node.inputs["Fac"]) | |
# links end | |
return material, node_dict | |
def setup_scene(): | |
fps = 30 | |
loop_sec = 6 | |
frame_count = fps * loop_sec | |
day = "324" | |
scene_num = 1 | |
bpy.context.scene.render.image_settings.file_format = "PNG" | |
bpy.context.scene.render.filepath = f"/tmp/day{day}_{scene_num}/" | |
seed = 1634522379.592337 | |
if seed: | |
random.seed(seed) | |
else: | |
seed = time_seed() | |
# Utility Building Blocks | |
delete_all_objects() | |
set_scene_props(fps, loop_sec) | |
loc = (-8.022, -6.212, 7.46) | |
rot = (1.299, -0.0, -0.912) | |
cam_empty = setup_camera(loc, rot) | |
cam_empty.location.z = 4.5 | |
context = { | |
"frame_count": frame_count, | |
} | |
context["colors"] = get_color_palette() | |
enable_extra_curves() | |
return context | |
def main(): | |
""" | |
Python code for this art project | |
https://www.artstation.com/artwork/48wX6L | |
""" | |
context = setup_scene() | |
gen_scene(context) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment