###########################################################################
#
# Python Script To Read Data From A CSV File & Create Animated 3D Bars
#
# This script is originally written by 5 Minutes Blender YouTube channel
#
###########################################################################
import bpy
import math
import random
import csv
context = bpy.context
scene = context.scene
###########################################################################
###########################################################################
#
# CHANGE THE FOLLOWING INPUT AS PER YOUR CSV FILE
# BE CAREFUL - ANY WRONG INPUT WILL RAISE AN ERROR.
csv_file_path = r"C:\MyTest\Database.csv"
font_file_path = r"C:\MyTest\Arialbd.ttf"
rank_column = 1
name_column = 2
data_column = 3
display_data_column1 = 5
display_data_column2 = 6
display_pic_column = 7
anim_speed_factor = 2 # Enter a value between 1 (slow) and 3 (fast)
###########################################################################
###########################################################################
anim_start_frame = 1
camera_start_frame = 21
data_start_position = 0
distance_bet_data = 3
plate_duration = 6
plate_step_duration = 2
plate_thickness = 0.3
max_moving_plates = 25
data_norm_factor = 5
min_diff_factor = 20
max_diff_factor = 12 * plate_thickness
min_data_height = 6
# Save the current location of the 3D cursor
saved_cursor_loc = scene.cursor.location.xyz
# Initialize the variables.
data_counter = 0
anim_curr_frame = anim_start_frame
speed_multiplier = 10 - anim_speed_factor
anim_length = speed_multiplier * 7
anim_interval = speed_multiplier * 7
rank_list = []
name_list = []
data_list = []
display_data_list1 = []
display_data_list2 = []
display_pic_list = []
# Read the csv file and store the data in an array
with open(csv_file_path, 'r') as file:
csvreader = csv.DictReader(file)
for row in csvreader:
key = str(list(row)[rank_column-1])
rank_list.append(int(row[key]))
key = str(list(row)[name_column-1])
name_list.append(str(row[key]))
key = str(list(row)[data_column-1])
data_list.append(float(row[key]))
key = str(list(row)[display_data_column1-1])
display_data_list1.append(str(row[key]))
key = str(list(row)[display_data_column2-1])
display_data_list2.append(str(row[key]))
key = str(list(row)[display_pic_column-1])
display_pic_list.append(str(row[key]))
number_of_data = len(name_list)
data_height_mean = sum(data_list) / len(data_list)
# Create a normalized data list
normalized_data_list = [data_list[count] * data_norm_factor/data_height_mean for count in range(number_of_data)]
if (min(normalized_data_list) < min_data_height):
mandatory_increment = min_data_height - min(normalized_data_list)
normalized_data_list = [x + mandatory_increment for x in normalized_data_list]
height_threshold = (max(normalized_data_list) - min(normalized_data_list)) / min_diff_factor
# Maintain a minimum & a maximum height-difference between two successive data bars
for i in range(number_of_data-1):
if (normalized_data_list[i] < normalized_data_list[i+1]) and \
(normalized_data_list[i+1] - normalized_data_list[i] < height_threshold):
for j in range(i+1,number_of_data):
normalized_data_list[j] += height_threshold
elif (normalized_data_list[i] > normalized_data_list[i+1]) and \
(normalized_data_list[i] - normalized_data_list[i+1] < height_threshold):
for j in range(i+1,number_of_data):
normalized_data_list[j] -= height_threshold
elif (normalized_data_list[i] < normalized_data_list[i+1]) and \
(normalized_data_list[i+1] - normalized_data_list[i] > max_diff_factor):
height_adjustment = normalized_data_list[i+1] - normalized_data_list[i] - max_diff_factor
for j in range(i+1,number_of_data):
normalized_data_list[j] -= height_adjustment
elif (normalized_data_list[i] > normalized_data_list[i+1]) and \
(normalized_data_list[i] - normalized_data_list[i+1] > max_diff_factor):
height_adjustment = normalized_data_list[i] - normalized_data_list[i+1] - max_diff_factor
for j in range(i+1,number_of_data):
normalized_data_list[j] += height_adjustment
# Create a new material for the bars
material_1 = bpy.data.materials.new(name="anim_material_1")
material_1.use_nodes = True
if material_1.node_tree:
material_1.node_tree.links.clear()
material_1.node_tree.nodes.clear()
nodes = material_1.node_tree.nodes
links = material_1.node_tree.links
output = nodes.new(type='ShaderNodeOutputMaterial')
shader = nodes.new(type='ShaderNodeBsdfPrincipled')
shader.location = (-300, 0)
shader.inputs[0].default_value = (0.15, 0.0, 0.0, 1)
links.new(shader.outputs[0], output.inputs[0])
# Create a new material for the text
material_2 = bpy.data.materials.new(name="anim_material_2")
material_2.use_nodes = True
if material_2.node_tree:
material_2.node_tree.links.clear()
material_2.node_tree.nodes.clear()
nodes = material_2.node_tree.nodes
links = material_2.node_tree.links
output = nodes.new(type='ShaderNodeOutputMaterial')
shader = nodes.new(type='ShaderNodeBsdfPrincipled')
shader.location = (-300, 0)
shader.inputs[0].default_value = (1, 1, 1, 1)
links.new(shader.outputs[0], output.inputs[0])
# Add an empty and set its initial position
bpy.ops.object.empty_add()
empty = bpy.context.active_object
empty.location = [0,0,10]
empty.hide_set(True)
empty.hide_render = True
# Set initial camera position
camera = bpy.data.objects['Camera']
camera.location = [-3,-15,10]
camera.rotation_euler = [math.radians(80),0,0]
bpy.data.cameras['Camera'].passepartout_alpha = 1.0
bpy.data.cameras['Camera'].clip_start = 0.1
bpy.data.cameras['Camera'].clip_end = 1000
# Add a track-to constraint for the camera
camera.animation_data_clear()
camera.constraints.clear()
track_to = camera.constraints.new('TRACK_TO')
track_to.target = empty
track_to.track_axis = 'TRACK_NEGATIVE_Z'
track_to.up_axis = 'UP_Y'
# Set light properties
Light = bpy.data.objects['Light']
Light.data.type = "SUN"
Light.data.energy = 4
Light.data.use_shadow = True
Light.data.use_contact_shadow = True
Light.data.contact_shadow_distance = 1.2
Light.rotation_euler = [math.radians(37),math.radians(3),math.radians(40)]
# Create new collections
new_col1 = bpy.data.collections.new(name="Data Bars")
new_col2 = bpy.data.collections.new(name="Data Names")
new_col3 = bpy.data.collections.new(name="Data Numbers")
new_col4 = bpy.data.collections.new(name="Data Pics")
new_col5 = bpy.data.collections.new(name="Data Plates")
new_col6 = bpy.data.collections.new(name="Ranks")
bpy.context.scene.collection.children.link(new_col1)
bpy.context.scene.collection.children.link(new_col2)
bpy.context.scene.collection.children.link(new_col3)
bpy.context.scene.collection.children.link(new_col4)
bpy.context.scene.collection.children.link(new_col5)
bpy.context.scene.collection.children.link(new_col6)
# Create the bars in a loop
while (data_counter < number_of_data):
data_height = normalized_data_list[data_counter]
data_rank = str(rank_list[data_counter])
data_name = str(name_list[data_counter])
display_data1 = str(display_data_list1[data_counter])
display_data2 = str(display_data_list2[data_counter])
# Add a cube and set its dimensions
bpy.ops.mesh.primitive_cube_add()
obj = bpy.context.active_object
obj.name = "Bar_" + str(data_counter+1)
obj.dimensions = [1.5,1,data_height]
obj.location = [data_start_position,0,data_height/2]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in obj.users_collection:
other_col.objects.unlink(obj)
if obj.name not in new_col1.objects:
new_col1.objects.link(obj)
# Set origin to the bottom of the cube
scene.cursor.location = (data_start_position,0,0)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
# Add a bevel modifier
bevel_mod = obj.modifiers.new(name="Bevelmodifier", type='BEVEL')
bevel_mod.width = 0.05
bevel_mod.segments = 10
# Calculate the correct plate thickness, no. of plates & animation duration & interval
num_of_plates = int(data_height/plate_thickness)
plate_thickness = data_height/num_of_plates
num_of_moving_plates = num_of_plates
if (num_of_moving_plates > max_moving_plates):
num_of_moving_plates = max_moving_plates
# Animate the height of the cube and also move the empty & the camera
anim_curr_frame += anim_length-1
obj.dimensions = [1.4,0.9,0]
obj.keyframe_insert(data_path="scale", frame = anim_curr_frame)
anim_curr_frame += 1
obj.dimensions = [1.4,0.9,data_height]
obj.keyframe_insert(data_path="scale", frame = anim_curr_frame)
anim_curr_frame += 10
obj.dimensions = [1.5,1,data_height]
obj.keyframe_insert(data_path="scale", frame = anim_curr_frame)
if (data_counter == number_of_data - 1):
empty.location = [data_start_position,0,data_height-0.75]
empty.keyframe_insert(data_path="location", frame = anim_curr_frame - 20)
camera.location = [data_start_position,-15,data_height]
camera.keyframe_insert(data_path="location", frame = anim_curr_frame - 20)
else:
empty.location = [data_start_position,0,data_height-0.75]
empty.keyframe_insert(data_path="location", frame = anim_curr_frame - 20)
camera.location = [data_start_position,-15,data_height]
camera.keyframe_insert(data_path="location", frame = anim_curr_frame - 20)
next_data_height = normalized_data_list[data_counter+1]
adjustment = anim_length - anim_length * min(int(next_data_height/plate_thickness), max_moving_plates) / max_moving_plates
empty.location = [data_start_position+0.25,0,data_height-0.75]
empty.keyframe_insert(data_path="location", frame = anim_curr_frame + anim_interval*2/3 + adjustment + (3-anim_speed_factor))
camera.location = [data_start_position+0.25,-15,data_height]
camera.keyframe_insert(data_path="location", frame = anim_curr_frame + anim_interval*2/3 + adjustment + (3-anim_speed_factor))
next_target_position = next_data_height - min(int(next_data_height/plate_thickness), max_moving_plates) * plate_thickness
empty.location = [data_start_position+distance_bet_data,0,next_target_position+2]
empty.keyframe_insert(data_path="location", frame = anim_curr_frame + anim_interval + 10 + (3-anim_speed_factor)*3)
camera.location = [data_start_position+distance_bet_data,-15,next_target_position+3]
camera.keyframe_insert(data_path="location", frame = anim_curr_frame + anim_interval + 10 + (3-anim_speed_factor)*3)
if (data_counter == 0):
empty.location = [data_start_position,0,1.2]
empty.keyframe_insert(data_path="location", frame = anim_start_frame)
camera.location = [data_start_position,-25,2]
camera.keyframe_insert(data_path="location", frame = anim_start_frame)
empty.location = [data_start_position,0,1.2]
empty.keyframe_insert(data_path="location", frame = camera_start_frame)
camera.location = [data_start_position,-15,2]
camera.keyframe_insert(data_path="location", frame = camera_start_frame)
obj.hide_viewport = True
obj.hide_render = True
obj.hide_set(True)
plate_anim_start = anim_curr_frame - 10 - (plate_duration + plate_step_duration * (num_of_moving_plates - 1))
plate_anim_frame = plate_anim_start
plate_list = []
# Create the plates in a loop and animate them
for count in range(num_of_plates):
bpy.ops.mesh.primitive_cube_add()
plate = bpy.context.active_object
plate.name = "Plate_" + str(data_counter+1) + "_" + str(count+1)
plate.dimensions = [1.49,0.99,plate_thickness]
plate.location = [data_start_position,0,data_height+3]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in plate.users_collection:
other_col.objects.unlink(plate)
if plate.name not in new_col5.objects:
new_col5.objects.link(plate)
# Add a bevel modifier
bevel_mod = plate.modifiers.new(name="Bevelmodifier", type='BEVEL')
bevel_mod.width = 0.05
bevel_mod.segments = 10
# Animate the plate
random_start_loc = random.uniform(2.5, 3.5)
plate.location = [data_start_position,0,data_height+random_start_loc]
plate.keyframe_insert(data_path="location", frame = plate_anim_frame)
if (num_of_plates - count <= num_of_moving_plates):
plate_anim_frame += plate_duration # No increment if it is not a moving plate
plate.location = [data_start_position,0,plate_thickness*(count+1)-(plate_thickness/2)]
plate.keyframe_insert(data_path="location", frame = plate_anim_frame)
if (num_of_plates - count <= num_of_moving_plates):
plate_anim_frame += -plate_duration # No change if it is not a moving plate
plate_anim_frame += plate_step_duration # No change if it is not a moving plate
# Hide render visibility until it is needed.
if (num_of_plates - count <= num_of_moving_plates):
plate.hide_render = True
plate.keyframe_insert(data_path="hide_render", frame = plate_anim_start-1)
plate.hide_render = False
plate.keyframe_insert(data_path="hide_render", frame = plate_anim_start)
else:
plate.hide_render = True
plate.keyframe_insert(data_path="hide_render", frame = plate_anim_start-21)
plate.hide_render = False
plate.keyframe_insert(data_path="hide_render", frame = plate_anim_start-20)
# Assign the first material created above
plate.data.materials.append(material_1)
# Create a duplicate copy of the plate
bpy.ops.object.duplicate(linked=False)
plate_copy = bpy.context.active_object
plate_list.append(plate_copy)
# Merge plate copies into a single object and animate the same
bpy.ops.object.select_all(action='DESELECT')
for each_plate in plate_list:
each_plate.select_set(True)
context.view_layer.objects.active = plate_list[0]
bpy.ops.object.join()
# Set origin to the middle of the cube
scene.cursor.location = (data_start_position,0,data_height/2)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
joined_obj = plate_list[0]
joined_obj.name = "Plate_" + str(data_counter+1) + "_merged"
bpy.ops.object.select_all(action='DESELECT')
joined_obj.animation_data_clear()
joined_obj.hide_viewport = True
joined_obj.hide_render = True
joined_obj.hide_set(True)
# Animate the z-location of the merged object
plate_anim_start = anim_curr_frame - 10 - (plate_duration + plate_step_duration * (num_of_moving_plates - 2))
plate_anim_frame = plate_anim_start
joined_obj.location = [data_start_position,0,(data_height/2) - (plate_thickness * num_of_moving_plates)]
joined_obj.keyframe_insert(data_path="location", frame = plate_anim_frame)
for count in range(num_of_moving_plates):
if (count == 0):
plate_anim_frame += plate_duration
else:
plate_anim_frame += plate_step_duration
joined_obj.location = [data_start_position,0,(data_height/2) - (plate_thickness * (num_of_moving_plates-(count+1)))]
joined_obj.keyframe_insert(data_path="location", frame = plate_anim_frame)
# Add the rank text
bpy.ops.object.text_add()
ob = bpy.context.active_object
ob.name = "Rank_" + str(data_counter+1)
ob.data.body = data_rank
ob.data.align_x = "CENTER"
ob.data.align_y = "BOTTOM_BASELINE"
ob.data.extrude = 0.1
ob.data.font = bpy.data.fonts.load(font_file_path)
ob.location = [data_start_position,0,data_height]
ob.scale = [2, 2, 2]
ob.rotation_euler = [math.radians(90),0,math.radians(15)]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in ob.users_collection:
other_col.objects.unlink(ob)
if ob.name not in new_col6.objects:
new_col6.objects.link(ob)
# Animate the z-scale of the rank text
ob.scale = [1,0,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame - 10)
ob.scale = [1,1.1,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame - 2)
ob.scale = [1,0.95,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame + 3)
ob.scale = [1,1.025,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame + 6)
ob.scale = [1,0.985,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame + 8)
ob.scale = [1,1,1]
ob.keyframe_insert(data_path="scale", frame = anim_curr_frame + 10)
# Hide render visibility until it is needed.
ob.hide_render = True
ob.keyframe_insert(data_path="hide_render", frame = anim_curr_frame-11)
ob.hide_render = False
ob.keyframe_insert(data_path="hide_render", frame = anim_curr_frame-10)
# Assign the second material created above
ob.data.materials.append(material_2)
# Add the 1st caption
bpy.ops.object.text_add()
ob = bpy.context.active_object
ob.name = "Name_" + str(data_counter+1)
ob.data.body = data_name
ob.data.align_x = "CENTER"
ob.data.align_y = "CENTER"
ob.data.extrude = 0.01
ob.data.font = bpy.data.fonts.load(font_file_path)
ob.location = [data_start_position,-0.7,data_height-6.5*plate_thickness]
ob.scale = [0.4, 0.4, 0.1]
ob.rotation_euler = [math.radians(90),0,0]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in ob.users_collection:
other_col.objects.unlink(ob)
if ob.name not in new_col2.objects:
new_col2.objects.link(ob)
# Assign the second material created above
ob.data.materials.append(material_2)
# Add a shrinkwrap modifier
shrink_mod = ob.modifiers.new(name="Shrinkwrapmodifier", type='SHRINKWRAP')
shrink_mod.offset = 0.001
shrink_mod.target = obj
#bpy.ops.object.convert(target='MESH') # Needed for cycles render
# Add the 2nd caption
bpy.ops.object.text_add()
ob = bpy.context.active_object
ob.name = "Data_" + str(data_counter+1)
ob.data.body = display_data1
ob.data.align_x = "CENTER"
ob.data.align_y = "CENTER"
ob.data.extrude = 0.01
ob.data.font = bpy.data.fonts.load(font_file_path)
ob.location = [data_start_position,-0.7,data_height-7.5*plate_thickness]
ob.scale = [0.35, 0.35, 0.1]
ob.rotation_euler = [math.radians(90),0,0]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in ob.users_collection:
other_col.objects.unlink(ob)
if ob.name not in new_col3.objects:
new_col3.objects.link(ob)
# Assign the second material created above
ob.data.materials.append(material_2)
# Add a shrinkwrap modifier
shrink_mod = ob.modifiers.new(name="Shrinkwrapmodifier", type='SHRINKWRAP')
shrink_mod.offset = 0.001
shrink_mod.target = obj
#bpy.ops.object.convert(target='MESH') # Needed for cycles render
# Add the 3rd caption
bpy.ops.object.text_add()
ob = bpy.context.active_object
ob.name = "Data_" + str(data_counter+1)
ob.data.body = display_data2
ob.data.align_x = "CENTER"
ob.data.align_y = "CENTER"
ob.data.extrude = 0.01
ob.data.font = bpy.data.fonts.load(font_file_path)
ob.location = [data_start_position,-0.7,data_height-8.5*plate_thickness]
ob.scale = [0.35, 0.35, 0.1]
ob.rotation_euler = [math.radians(90),0,0]
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
for other_col in ob.users_collection:
other_col.objects.unlink(ob)
if ob.name not in new_col3.objects:
new_col3.objects.link(ob)
# Assign the second material created above
ob.data.materials.append(material_2)
# Add a shrinkwrap modifier
shrink_mod = ob.modifiers.new(name="Shrinkwrapmodifier", type='SHRINKWRAP')
shrink_mod.offset = 0.001
shrink_mod.target = obj
#bpy.ops.object.convert(target='MESH') # Needed for cycles render
# Add the display picture
bpy.ops.mesh.primitive_plane_add()
ob = bpy.context.active_object
ob.name = "Picture_" + str(data_counter+1)
ob.location = [data_start_position,-0.7,data_height-0.86]
ob.dimensions = [1.41, 1.6, 1]
ob.rotation_euler = [math.radians(90),0,0]
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
for other_col in ob.users_collection:
other_col.objects.unlink(ob)
if ob.name not in new_col4.objects:
new_col4.objects.link(ob)
# Create a new material and assign
dp_material = bpy.data.materials.new(name="anim_dp_material"+str(data_counter+1))
dp_material.use_nodes = True
if dp_material.node_tree:
dp_material.node_tree.links.clear()
dp_material.node_tree.nodes.clear()
nodes = dp_material.node_tree.nodes
links = dp_material.node_tree.links
output = nodes.new(type='ShaderNodeOutputMaterial')
shader = nodes.new(type='ShaderNodeBsdfPrincipled')
shader.location = (-300, 0)
shader.inputs[0].default_value = (0.05, 0.05, 0.05, 1)
shader.inputs[7].default_value = 0.05
texture = nodes.new(type='ShaderNodeTexImage')
texture.location = (-700, 0)
texture.image = bpy.data.images.load(display_pic_list[data_counter])
links.new(texture.outputs["Color"], shader.inputs["Base Color"])
links.new(texture.outputs["Alpha"], shader.inputs["Alpha"])
links.new(shader.outputs[0], output.inputs[0])
dp_material.blend_method = 'BLEND'
ob.data.materials.append(dp_material)
# Add a shrinkwrap modifier
shrink_mod = ob.modifiers.new(name="Shrinkwrapmodifier", type='SHRINKWRAP')
shrink_mod.offset = 0.001
shrink_mod.target = obj
# Hide render visibility until it is needed.
ob.hide_render = True
ob.keyframe_insert(data_path="hide_render", frame = anim_curr_frame)
ob.hide_render = False
ob.keyframe_insert(data_path="hide_render", frame = anim_curr_frame+1)
# Create a duplicate copy of the display picture
bpy.ops.object.duplicate(linked=False)
ob_copy = bpy.context.active_object
# Add a subdivision modifier and a shrinkwrap modifier
ob_copy.modifiers.clear()
subd_mod = ob_copy.modifiers.new(name="Subdivisionmodifier", type='SUBSURF')
subd_mod.subdivision_type = "SIMPLE"
subd_mod.levels = 6
subd_mod.render_levels = 6
shrink_mod = ob_copy.modifiers.new(name="Shrinkwrapmodifier", type='SHRINKWRAP')
shrink_mod.offset = 0.001
shrink_mod.target = joined_obj
# Hide render visibility after it's purpose is over.
ob_copy.hide_render = False
ob_copy.keyframe_insert(data_path="hide_render", frame = anim_curr_frame)
ob_copy.hide_render = True
ob_copy.keyframe_insert(data_path="hide_render", frame = anim_curr_frame+1)
#increase the loop counters
data_counter += 1
data_start_position += distance_bet_data
anim_curr_frame += anim_interval
# Clean-up work
# Reset 3D cursor location back to the original
scene.cursor.location.xyz = saved_cursor_loc
context.active_object.select_set(False)
# Set the current frame to frame# 1
scene.frame_set(1)
# Set the scene length
scene.frame_start = 1
scene.frame_end = anim_curr_frame + 50
#https://drive.google.com/file/d/1XESugeNehYLBuyog8Qog0KwcJCIXpscs/view
#CSV-To-Blender-Data-Comparison-Special-Script.txt。