Created
October 21, 2024 08:22
-
-
Save sina-mansour/14c3ecff56b51d2ba5a3d0b2da7deec9 to your computer and use it in GitHub Desktop.
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
# Blender executed via: | |
# PYTHONPATH=/home/sina/miniconda3/envs/blender_env/bin/python blender --python-use-system-env | |
# Notes: | |
# Setting up blender to use a conda virtualenv: | |
# https://stackoverflow.com/questions/70639689/how-to-use-the-anaconda-environment-on-blender | |
# Blender Python API: | |
# https://docs.blender.org/api/current/info_quickstart.html | |
# Blender Python tips and tricks: | |
# https://docs.blender.org/api/current/info_tips_and_tricks.html | |
# Blender can be built from source to enable direct python execution: | |
# https://developer.blender.org/docs/handbook/building_blender/python_module/ | |
# Building blender: | |
# https://developer.blender.org/docs/handbook/building_blender/ | |
# Idea: conda can be used to build blender for a python package: | |
# https://docs.aws.amazon.com/deadline-cloud/latest/developerguide/create-conda-recipe-blender.html | |
# https://github.com/aws-deadline/deadline-cloud-samples/blob/mainline/conda_recipes/blender-4.2/recipe/meta.yaml | |
import bpy | |
import bmesh | |
import math | |
import numpy as np | |
import nibabel as nib | |
import matplotlib.pyplot as plt | |
from Connectome_Spatial_Smoothing import CSS as css | |
from cerebro import cerebro_brain_utils as cbu | |
from cerebro import cerebro_brain_viewer as cbv | |
# function to load brain surface geometries | |
def load_cortical_meshes(surface): | |
left_surface_file, right_surface_file = cbu.get_left_and_right_GIFTI_template_surface(surface) | |
left_vertices, left_triangles = cbu.load_GIFTI_surface(left_surface_file) | |
right_vertices, right_triangles = cbu.load_GIFTI_surface(right_surface_file) | |
return left_vertices, left_triangles, right_vertices, right_triangles | |
# function to read msa atlas colors | |
def read_msa_atlas_information(file_path): | |
with open(file_path, 'r') as file: | |
lines = file.readlines() | |
# Strip newlines and concatenate every two lines, then split | |
concatenated_lines = [(lines[i].strip() + ' ' + lines[i+1].strip()).split() for i in range(0, len(lines)-1, 2) if '7Networks_' not in lines[i]] | |
# Now make dict | |
information = { | |
float(line[1]): { | |
"name": line[0], | |
"r": float(line[2])/255, | |
"g": float(line[3])/255, | |
"b": float(line[4])/255, | |
"a": float(line[5])/255} | |
for line in concatenated_lines | |
} | |
return information | |
# function to clean the blender scene | |
def blender_clean_up(): | |
# make sure the active object is not in Edit Mode | |
if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": | |
bpy.ops.object.editmode_toggle() | |
# make sure non of the objects are hidden from the viewport, selection, or disabled | |
for obj in bpy.data.objects: | |
obj.hide_set(False) | |
obj.hide_select = False | |
obj.hide_viewport = False | |
# select all the object and delete them (just like pressing A + X + D in the viewport) | |
bpy.ops.object.select_all(action="SELECT") | |
bpy.ops.object.delete() | |
# find all the collections and remove them | |
collection_names = [col.name for col in bpy.data.collections] | |
for name in collection_names: | |
bpy.data.collections.remove(bpy.data.collections[name]) | |
# in the case when you modify the world shader | |
# delete and recreate the world object | |
world_names = [world.name for world in bpy.data.worlds] | |
for name in world_names: | |
bpy.data.worlds.remove(bpy.data.worlds[name]) | |
# create a new world data block | |
bpy.ops.world.new() | |
bpy.context.scene.world = bpy.data.worlds["World"] | |
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) | |
# function to adjust animation frame setup | |
def frame_setup(start_frame, end_frame): | |
# Set the start and end frames of the animation | |
bpy.context.scene.frame_start = start_frame | |
bpy.context.scene.frame_end = end_frame | |
# function to create a mesh | |
def create_mesh_object(vertices, faces, mesh_name, object_name): | |
# Create a new mesh and object | |
mesh = bpy.data.meshes.new(mesh_name) | |
obj = bpy.data.objects.new(object_name, mesh) | |
# Link the object to the current scene collection | |
bpy.context.collection.objects.link(obj) | |
# Set the object as active and select it | |
bpy.context.view_layer.objects.active = obj | |
obj.select_set(True) | |
# Create the mesh from the vertices and faces | |
mesh.from_pydata(vertices, [], faces) | |
mesh.update() # Update the mesh to make sure it's updated in the scene | |
# Return the created object | |
return mesh, obj | |
# function to apply smooth shading to object | |
def shade_smooth(obj): | |
# Ensure the object is selected and active | |
bpy.context.view_layer.objects.active = obj | |
obj.select_set(True) | |
# Apply smooth shading | |
bpy.ops.object.shade_smooth() | |
# function to turn data to colors | |
def generate_vertex_colors(data, surface_model, colormap=plt.cm.coolwarm, alpha=1.0): | |
# Normalize the data and map to colors | |
scaled_data = (data - data.min()) / (data.max() - data.min()) | |
vertex_data = np.zeros(surface_model.surface_number_of_vertices) * np.nan | |
vertex_data[np.array(surface_model.vertex_indices)] = scaled_data[ | |
surface_model.index_offset:surface_model.index_offset+surface_model.index_count | |
] | |
vertex_colors = colormap(vertex_data) | |
# adjust alpha | |
vertex_colors[:, 3] = alpha | |
return vertex_colors | |
# Function to update vertex colors on a mesh | |
def update_vertex_colors(mesh, vertex_colors): | |
color_layer = mesh.vertex_colors.active.data | |
for loop_index, loop in enumerate(mesh.loops): | |
vertex_index = loop.vertex_index | |
color_layer[loop_index].color = vertex_colors[vertex_index] | |
# Function to add vertex colors to a mesh's vertex color layer | |
def create_layer(mesh, layer_name): | |
# Create a new vertex color layer or use an existing one | |
if layer_name not in mesh.vertex_colors: | |
mesh.vertex_colors.new(name=layer_name) | |
vertex_color_layer = mesh.vertex_colors[layer_name] | |
mesh.vertex_colors.active_index = [x[0] for x in mesh.vertex_colors.items()].index(layer_name) | |
return vertex_color_layer | |
# Function to add vertex colors to a mesh's vertex color layer | |
def add_vertex_colors_to_layer(mesh, vertex_colors, layer_name): | |
create_layer(mesh, layer_name) | |
update_vertex_colors(mesh, vertex_colors) | |
mesh.update() | |
# Function to create material and assign color to it | |
def assign_object_material_with_color(obj, color_layer_name, metallic, roughness): | |
# Create a new material if it doesn't exist | |
material = bpy.data.materials.new("VertexColorMaterial") | |
material.use_nodes = True | |
nodes = material.node_tree.nodes | |
links = material.node_tree.links | |
# Clear existing nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Create nodes: Vertex Color node, Principled BSDF, and Material Output | |
vertex_color_node = nodes.new(type='ShaderNodeVertexColor') | |
vertex_color_node.layer_name = color_layer_name | |
principled_bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
material_output = nodes.new(type='ShaderNodeOutputMaterial') | |
# Connect vertex color layer to the Principled BSDF shader and then to the material output | |
links.new(vertex_color_node.outputs['Color'], principled_bsdf.inputs['Base Color']) | |
links.new(vertex_color_node.outputs['Alpha'], principled_bsdf.inputs['Alpha']) | |
links.new(principled_bsdf.outputs['BSDF'], material_output.inputs['Surface']) | |
# Other material properties | |
principled_bsdf.inputs["Metallic"].default_value = metallic | |
principled_bsdf.inputs["Roughness"].default_value = roughness | |
# Assign the material to the object | |
if len(obj.data.materials) > 0: | |
obj.data.materials[0] = material | |
else: | |
obj.data.materials.append(material) | |
return material | |
# Function to create object | |
def create_data_object(name, object_data): | |
obj = bpy.data.objects.new(name, object_data) | |
bpy.context.collection.objects.link(obj) | |
return obj | |
# Function to create empty | |
def create_empty(name, location): | |
# Create an empty object | |
bpy.ops.object.empty_add(type='PLAIN_AXES', location=location) | |
empty = bpy.context.object | |
empty.name = name | |
return empty | |
# Function to create plane | |
def create_plane(name, scale, location, rotation, subdivision_kws={}): | |
# Create a plane | |
bpy.ops.mesh.primitive_plane_add(location=location, rotation=rotation) | |
plane = bpy.context.object | |
plane.name = name | |
plane.scale[0] = scale[0] / 2 | |
plane.scale[1] = scale[1] / 2 | |
# subdivide if needed | |
if subdivision_kws['subdivide']: | |
# Enter edit mode to further subdivide | |
bpy.ops.object.mode_set(mode='EDIT') | |
bm = bmesh.from_edit_mesh(plane.data) | |
# Subdivide the mesh further to get 80 sections | |
bmesh.ops.subdivide_edges(bm, edges=bm.edges, cuts=subdivision_kws['cuts'], use_grid_fill=True,) | |
bmesh.update_edit_mesh(plane.data) | |
bpy.ops.object.mode_set(mode='OBJECT') | |
return plane | |
# Function to create a brain like material | |
def create_text_object(name, location, rotation, text, align_x="CENTER", align_y="CENTER", font=None): | |
bpy.ops.object.text_add(location=location, rotation=rotation) | |
text_obj = bpy.context.object | |
text_obj.name = name | |
# Set the text content | |
text_obj.data.body = text | |
# Align the text within its local coordinates | |
text_obj.data.align_x = align_x | |
text_obj.data.align_y = align_y | |
# Optional: Load a custom font | |
if font: | |
text_obj.data.font = bpy.data.fonts.load(font) | |
return text_obj | |
# Function to group a subset of plane vertices | |
def group_top_vertices(plane, group_name, epsilon=0.01): | |
# Create a vertex group for the top central vertices | |
vertex_group = plane.vertex_groups.new(name=group_name) | |
# Get the top vertices (considering Z direction) | |
top_z_coord = np.min([v.co.x for v in plane.data.vertices]) | |
max_y_coord = np.max([v.co.y for v in plane.data.vertices]) | |
mid_y_coord = np.mean([v.co.y for v in plane.data.vertices]) | |
y_selection_lim = (max_y_coord - mid_y_coord) / 3 | |
top_verts = [ | |
v for v in plane.data.vertices | |
if ( | |
(np.abs(v.co.x - top_z_coord) < epsilon) and | |
(np.abs(v.co.y - mid_y_coord) <= y_selection_lim) | |
) | |
] | |
# Assign the top vertices to the vertex group | |
for v in top_verts: | |
vertex_group.add([v.index], 1.0, 'ADD') | |
return vertex_group | |
# Function to set a hook modifier on vertex group | |
def set_hook_modifier(object_to_be_hooked, vertex_group_name, hook_object): | |
# Add a hook modifier to the plane and assign the top vertices | |
hook_modifier = object_to_be_hooked.modifiers.new(name="Hook", type='HOOK') | |
hook_modifier.object = hook_object | |
hook_modifier.vertex_group = vertex_group_name | |
return hook_modifier | |
# Function to add cloth physics to object | |
def add_cloth_physics(object, pin_vertex_group=None, vertex_mass=1): | |
# Ensure the object is a mesh | |
if object.type != 'MESH': | |
print(f"Error: {object.name} is not a mesh object.") | |
return | |
# Add Cloth modifier | |
cloth_modifier = object.modifiers.new(name="Cloth", type='CLOTH') | |
# Optional: Set the pinning vertex group (if specified) | |
if pin_vertex_group is not None and pin_vertex_group in object.vertex_groups: | |
cloth_modifier.settings.vertex_group_mass = pin_vertex_group | |
# Default settings for cloth simulation (you can customize these as needed) | |
cloth_modifier.settings.quality = 30 # Quality of the simulation (higher is slower but more accurate) | |
cloth_modifier.settings.time_scale = 5 # Speed Multiplier | |
cloth_modifier.settings.mass = vertex_mass # Mass of the cloth | |
cloth_modifier.settings.air_damping = 1 # Air resistance | |
cloth_modifier.settings.tension_stiffness = 10 # Stiffness of the cloth | |
cloth_modifier.settings.compression_stiffness = 10 | |
cloth_modifier.settings.shear_stiffness = 5 | |
cloth_modifier.settings.bending_stiffness = 0.2 | |
# Enable self-collision to prevent parts of the cloth from intersecting itself | |
cloth_modifier.collision_settings.use_self_collision = True | |
cloth_modifier.collision_settings.self_friction = 5 # Friction during self-collision | |
cloth_modifier.collision_settings.self_distance_min = 0.015 # Minimum distance between self-collisions (tweak if needed) | |
return cloth_modifier | |
# Function to add collision physics to object | |
def add_collision_physics(object): | |
# Ensure the object is a mesh | |
if object.type != 'MESH': | |
print(f"Error: {object.name} is not a mesh object.") | |
return | |
# Add collision modifier | |
collision_modifier = object.modifiers.new(name="Collision", type='COLLISION') | |
# Set default collision settings (you can customize these) | |
collision_modifier.settings.thickness_outer = 0.8 # Outer thickness of collision boundary | |
collision_modifier.settings.thickness_inner = 0.8 # Inner thickness of collision boundary | |
collision_modifier.settings.damping = 0.5 # Damping factor for collision | |
collision_modifier.settings.cloth_friction = 0.5 # Friction factor for collision | |
# Function to add subdivision modifier | |
def add_subdivision_modifier(obj, levels=3): | |
# Ensure the object is a mesh | |
if obj.type != 'MESH': | |
print(f"Error: {obj.name} is not a mesh object.") | |
return | |
# Add Cloth modifier | |
subdivision_modifier = obj.modifiers.new(name="Subdivision", type='SUBSURF') | |
# Set subdivision settings | |
subdivision_modifier.levels = levels # Viewport levels | |
subdivision_modifier.render_levels = levels # render levels | |
# Function makes a thin film of the mesh | |
def add_solidify_modifier(obj, thickness=0.001, offset=None): | |
# Add a Solidify modifier for thickness | |
solidify_mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY') | |
# Set the thickness | |
solidify_mod.thickness = thickness | |
# Optional: Set other solidify settings | |
if offset is not None: | |
solidify_mod.offset = offset | |
else: | |
solidify_mod.offset = -1.0 | |
solidify_mod.use_rim = True # Fill in any open edges | |
def add_corrective_smooth_modifier(obj): | |
# Ensure the object is a mesh | |
if obj.type != 'MESH': | |
print(f"Error: {obj.name} is not a mesh object.") | |
return | |
# Add a corrective smooth modifier | |
csmooth_mod = obj.modifiers.new(name="CorrectiveSmooth",type='CORRECTIVE_SMOOTH') | |
# Adjust parameters | |
csmooth_mod.factor = 1 | |
csmooth_mod.iterations = 10 | |
csmooth_mod.scale = 0 | |
# Function to apply auto smooth shading | |
def enable_auto_smooth_shading(obj, angle=30.0): | |
# Ensure the object is selected and active | |
bpy.context.view_layer.objects.active = obj | |
obj.select_set(True) | |
# Enable Auto Smooth for the object | |
bpy.ops.object.shade_auto_smooth() | |
# Function to create a silk like material | |
def create_fleshy_bsdf(nodes, location, base_color): | |
# Add a Principled BSDF shader node | |
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
bsdf.location = location | |
# Set the base color to royal red | |
bsdf.inputs['Base Color'].default_value = base_color # RGB + Alpha (Red with a bit of saturation) | |
# Set roughness | |
bsdf.inputs['Roughness'].default_value = 0.3 # Lower roughness makes the surface shiny | |
# Set IOR | |
bsdf.inputs['IOR'].default_value = 4. | |
# Set metallic | |
bsdf.inputs['Metallic'].default_value = 0. | |
# Specular and sheen-related properties | |
bsdf.inputs['Specular IOR Level'].default_value = 0.5 | |
bsdf.inputs['Specular Tint'].default_value = (1, 0.1, 0.1, 1) | |
# Sheen settings | |
bsdf.inputs['Sheen Weight'].default_value = 0.8 # Moderate sheen | |
bsdf.inputs['Sheen Roughness'].default_value = 0.1 # Smooth sheen | |
bsdf.inputs['Sheen Tint'].default_value = (0.9, 0.05, 0.05, 1) # Slight red tint to sheen | |
# Subsurface settings | |
bsdf.inputs['Subsurface Weight'].default_value = 0.8 | |
bsdf.inputs['Subsurface Radius'].default_value = (1.2, 0.8, 0.6) | |
bsdf.inputs['Subsurface Scale'].default_value = 1 | |
bsdf.inputs['Subsurface Anisotropy'].default_value = 0.8 | |
return bsdf | |
# Function to create a silk like material | |
def create_brain_material(): | |
# Create a new material | |
brain_material = bpy.data.materials.new(name="brain_material") | |
# Enable the use of nodes (necessary for Principled BSDF) | |
brain_material.use_nodes = True | |
# Get the material's node tree | |
nodes = brain_material.node_tree.nodes | |
links = brain_material.node_tree.links | |
# Clear any default nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Add a Principled BSDF shader node | |
bsdf = create_fleshy_bsdf(nodes, location=(0, 0), base_color=(0.95, 0.3, 0.3, 1)) | |
bsdf2 = create_fleshy_bsdf(nodes, location=(0, -400), base_color=(0.3, 0.02, 0.02, 1)) | |
# Mix the two | |
mixshader = nodes.new(type='ShaderNodeMixShader') | |
mixshader.location = (300, 0) | |
# mixshader.inputs['Fac'].default_value = 0. | |
links.new(bsdf.outputs['BSDF'], mixshader.inputs[1]) | |
links.new(bsdf2.outputs['BSDF'], mixshader.inputs[2]) | |
# two color ramps for voronois | |
cramp1 = nodes.new(type='ShaderNodeValToRGB') | |
cramp1.location = (-300, 400) | |
cramp1.color_ramp.elements[0].position = 0.008 | |
cramp1.color_ramp.elements[0].color = (1., 1., 1., 1.) | |
cramp1.color_ramp.elements[1].position = 0.01 | |
cramp1.color_ramp.elements[1].color = (0., 0., 0., 1.) | |
cramp2 = nodes.new(type='ShaderNodeValToRGB') | |
cramp2.location = (-300, 0) | |
cramp2.color_ramp.elements[0].position = 0.1 | |
cramp2.color_ramp.elements[0].color = (1., 1., 1., 1.) | |
cramp2.color_ramp.elements[1].position = 0.04 | |
cramp2.color_ramp.elements[1].color = (0., 0., 0., 1.) | |
# two voronois | |
voronoi1 = nodes.new(type='ShaderNodeTexVoronoi') | |
voronoi1.location = (-500, 400) | |
voronoi1.feature = 'DISTANCE_TO_EDGE' | |
voronoi1.inputs['Scale'].default_value = 3. | |
voronoi1.inputs['Detail'].default_value = 2. | |
links.new(voronoi1.outputs['Distance'], cramp1.inputs['Fac']) | |
voronoi2 = nodes.new(type='ShaderNodeTexVoronoi') | |
voronoi2.location = (-500, 0) | |
voronoi2.feature = 'DISTANCE_TO_EDGE' | |
voronoi2.inputs['Scale'].default_value = 30. | |
links.new(voronoi2.outputs['Distance'], cramp2.inputs['Fac']) | |
# Noise to wobble voronoi result | |
noise = nodes.new(type='ShaderNodeTexNoise') | |
noise.location = (-700, 200) | |
noise.inputs['Scale'].default_value = 3. | |
noise.inputs['Detail'].default_value = 15. | |
links.new(noise.outputs['Color'], voronoi1.inputs['Vector']) | |
links.new(noise.outputs['Color'], voronoi2.inputs['Vector']) | |
# Add a math node to mix | |
mixmath = nodes.new(type='ShaderNodeMath') | |
mixmath.location = (0, 200) | |
links.new(cramp1.outputs['Color'], mixmath.inputs[0]) | |
links.new(cramp2.outputs['Color'], mixmath.inputs[1]) | |
links.new(mixmath.outputs['Value'], mixshader.inputs['Fac']) | |
# Add a displacement node | |
displace = nodes.new(type='ShaderNodeDisplacement') | |
displace.location = (300, 200) | |
displace.inputs['Scale'].default_value = 0.01 | |
links.new(mixmath.outputs['Value'], displace.inputs['Height']) | |
# Add an Output node and link it to the BSDF | |
output = nodes.new(type='ShaderNodeOutputMaterial') | |
output.location = (500, 0) | |
links.new(mixshader.outputs['Shader'], output.inputs['Surface']) | |
links.new(displace.outputs['Displacement'], output.inputs['Displacement']) | |
# Let's create a bump node to improve texture | |
bump = nodes.new(type='ShaderNodeBump') | |
bump.location = (-200, -300) | |
bump.inputs['Strength'].default_value = 0.8 | |
links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) | |
links.new(bump.outputs['Normal'], bsdf2.inputs['Normal']) | |
links.new(mixmath.outputs['Value'], bump.inputs['Height']) | |
# show bump and displacement | |
brain_material.displacement_method = 'BOTH' | |
return brain_material | |
# Function to create a brain like material | |
def create_silk_material(): | |
# Create a new material | |
silk_material = bpy.data.materials.new(name="Royal_Red_Silk") | |
# Enable the use of nodes (necessary for Principled BSDF) | |
silk_material.use_nodes = True | |
# Get the material's node tree | |
nodes = silk_material.node_tree.nodes | |
links = silk_material.node_tree.links | |
# Clear any default nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Add a Principled BSDF shader node | |
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
bsdf.location = (0, 0) | |
# Set the base color to royal red | |
bsdf.inputs['Base Color'].default_value = (0.3, 0.01, 0.03, 1) # RGB + Alpha (Red with a bit of saturation) | |
# Set a low roughness for silk-like smoothness | |
bsdf.inputs['Roughness'].default_value = 0.5 # Lower roughness makes the surface shiny | |
# Set a low roughness for silk-like material | |
bsdf.inputs['Metallic'].default_value = 0.7 | |
# # Specular and sheen-related properties for silk fabric | |
# bsdf.inputs['Specular IOR Level'].default_value = 0.8 # Adjust specular reflection | |
# bsdf.inputs['Specular Tint'].default_value = (1, 0.1, 0.1, 1) # White specular reflection | |
# # Anisotropy for directional fabric-like sheen | |
# bsdf.inputs['Anisotropic'].default_value = 0.8 # High anisotropy | |
# bsdf.inputs['Anisotropic Rotation'].default_value = 0.2 # Low anisotropic rotation | |
# # Sheen settings for fabric glow | |
# bsdf.inputs['Sheen Weight'].default_value = 0.5 # Moderate sheen | |
# bsdf.inputs['Sheen Roughness'].default_value = 0.1 # Smooth sheen | |
# bsdf.inputs['Sheen Tint'].default_value = (0.9, 0.05, 0.05, 1) # Slight red tint to sheen | |
# Add an Output node and link it to the BSDF | |
output = nodes.new(type='ShaderNodeOutputMaterial') | |
output.location = (400, 0) | |
links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) | |
return silk_material | |
# Function to create a brain like material | |
def create_glass_material(): | |
# Create a new material | |
glass_material = bpy.data.materials.new(name="glass_material") | |
# Enable the use of nodes (necessary for Principled BSDF) | |
glass_material.use_nodes = True | |
# Get the material's node tree | |
nodes = glass_material.node_tree.nodes | |
links = glass_material.node_tree.links | |
# Clear any default nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Add a Principled BSDF shader node | |
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
bsdf.location = (0, 0) | |
# Set the base color to white | |
bsdf.inputs['Base Color'].default_value = (1., 1., 1., 1) # RGB + Alpha (Red with a bit of saturation) | |
# Set roughness to control the smoothness (0 for perfect glass) | |
bsdf.inputs['Roughness'].default_value = 0.0 # No roughness for clear glass | |
# Set the IOR (Index of Refraction) for glass (~1.45-1.55) | |
bsdf.inputs['IOR'].default_value = 1.45 | |
# Set the transmission value to 1 for glass-like transparency | |
bsdf.inputs['Transmission Weight'].default_value = 1.0 # Full glass effect | |
bsdf.inputs['Alpha'].default_value = 0.1 # Also a bit more transparent | |
# Add an Output node and link it to the BSDF | |
output = nodes.new(type='ShaderNodeOutputMaterial') | |
output.location = (400, 0) | |
links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) | |
return glass_material | |
# Function to create a brain like material | |
def create_colored_material(color=(1,1,1,1), name='colored_material'): | |
# Create a new material | |
colored_material = bpy.data.materials.new(name=name) | |
# Enable the use of nodes (necessary for Principled BSDF) | |
colored_material.use_nodes = True | |
# Get the material's node tree | |
nodes = colored_material.node_tree.nodes | |
links = colored_material.node_tree.links | |
# Clear any default nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Add a Principled BSDF shader node | |
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
bsdf.location = (0, 0) | |
# Set the base color to white | |
bsdf.inputs['Base Color'].default_value = color | |
# Set roughness | |
bsdf.inputs['Roughness'].default_value = 1.0 | |
# Add an Output node and link it to the BSDF | |
output = nodes.new(type='ShaderNodeOutputMaterial') | |
output.location = (400, 0) | |
links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) | |
return colored_material | |
# Function to create a brain like material | |
def create_colored_light_material(color=(1,1,1,1), name='colored_material', emission_strength=1.): | |
# Create a new material | |
clight_material = bpy.data.materials.new(name=name) | |
# Enable the use of nodes (necessary for Principled BSDF) | |
clight_material.use_nodes = True | |
# Get the material's node tree | |
nodes = clight_material.node_tree.nodes | |
links = clight_material.node_tree.links | |
# Clear any default nodes | |
for node in nodes: | |
nodes.remove(node) | |
# Add a Principled BSDF shader node | |
bsdf = nodes.new(type='ShaderNodeBsdfPrincipled') | |
bsdf.location = (0, 0) | |
# Set the base color to white | |
bsdf.inputs['Base Color'].default_value = color | |
# Set roughness | |
bsdf.inputs['Roughness'].default_value = 1.0 | |
# Set emission | |
bsdf.inputs['Emission Color'].default_value = color | |
bsdf.inputs['Emission Strength'].default_value = emission_strength | |
# Add an Output node and link it to the BSDF | |
output = nodes.new(type='ShaderNodeOutputMaterial') | |
output.location = (400, 0) | |
links.new(bsdf.outputs['BSDF'], output.inputs['Surface']) | |
return clight_material | |
# Function to assign material to object | |
def assign_material_to_object(material, obj): | |
# Assign the material to the object | |
if obj.data.materials: | |
# If there are materials, assign to first slot | |
obj.data.materials[0] = material | |
else: | |
# If no materials, append a new one | |
obj.data.materials.append(material) | |
# Function to add a camera | |
def create_camera(camera_location, target_location): | |
camera_data = bpy.data.cameras.new(name="Camera") | |
camera_object = create_data_object("Camera", camera_data) | |
camera_object.location = camera_location | |
# Add a target (empty object) for the camera to focus on | |
target_object = create_data_object("CameraTarget", None) | |
target_object.location = target_location | |
# Add the "Track To" constraint to the camera to make it always look at the target | |
constraint = camera_object.constraints.new(type='TRACK_TO') | |
constraint.target = target_object | |
# parent to target to aid camera rotations | |
camera_object.parent = target_object | |
# Set the camera as the active camera for the scene | |
bpy.context.scene.camera = camera_object | |
return camera_object, target_object | |
# Function to add a sunlight | |
def create_sunlight(name, rotation, strength=1.0): | |
# Add a new sun light to the scene | |
light_data = bpy.data.lights.new(name=name, type='SUN') | |
light_object = create_data_object(name, light_data) | |
# Position the light in the scene | |
light_object.rotation_euler = rotation | |
# Set the strength of the light | |
light_data.energy = 1.0 # You can adjust this value to make the scene brighter or dimmer | |
return light_object | |
# Function to change background color and lighting | |
def set_background(background_color, ambient_light_strength=1.0): | |
bpy.context.scene.world.use_nodes = True | |
bg = bpy.context.scene.world.node_tree.nodes.get("Background") | |
bg.inputs[0].default_value = background_color | |
bg.inputs[1].default_value = ambient_light_strength # Strength of the ambient light | |
# Function to set HDRI background | |
def set_hdri_background(hdr_image_path): | |
# HDRI background | |
bpy.context.scene.world.use_nodes = True | |
node_tree = bpy.context.scene.world.node_tree | |
# Clear any existing nodes in the world | |
nodes = node_tree.nodes | |
nodes.clear() | |
# Add Background node and set it as the output | |
background_node = nodes.new(type="ShaderNodeBackground") | |
# Add Environment Texture node (for the HDRI) | |
env_texture_node = nodes.new(type="ShaderNodeTexEnvironment") | |
# Load your HDRI image here (replace with your file path) | |
env_texture_node.image = bpy.data.images.load(hdr_image_path) | |
# Add World Output node | |
world_output_node = nodes.new(type="ShaderNodeOutputWorld") | |
# Connect the Environment Texture node to the Background node | |
node_tree.links.new(env_texture_node.outputs["Color"], background_node.inputs["Color"]) | |
# Connect the Background node to the World Output node | |
node_tree.links.new(background_node.outputs["Background"], world_output_node.inputs["Surface"]) | |
# Optional: Adjust the strength of the HDRI light | |
background_node.inputs["Strength"].default_value = 1.5 # You can change this value | |
# Function to set the resolution of renders | |
def set_render_properties(resolution_x=1920, resolution_y=1080, resolution_percentage=100, fps=24): | |
bpy.context.scene.render.resolution_x = resolution_x # Width in pixels | |
bpy.context.scene.render.resolution_y = resolution_y # Height in pixels | |
bpy.context.scene.render.resolution_percentage = resolution_percentage # Scale | |
bpy.context.scene.render.fps = fps # frame rate | |
# Render a single frame to a PNG | |
def render_single_frame(frame, filepath, engine='BLENDER_EEVEE_NEXT', transparent=True): | |
# Set the current frame to render | |
bpy.context.scene.frame_set(frame) | |
# Set render output file path | |
bpy.context.scene.render.filepath = filepath | |
# Set render engine to 'CYCLES' or 'BLENDER_EEVEE_NEXT' | |
bpy.context.scene.render.engine = engine | |
# Set the file format to PNG | |
bpy.context.scene.render.image_settings.file_format = 'PNG' | |
# Make the background transparent | |
if transparent: | |
bpy.context.scene.render.film_transparent = True | |
bpy.context.scene.render.image_settings.color_mode = 'RGBA' # Enable alpha (transparency) | |
# Trigger the render and save the image | |
bpy.ops.render.render(write_still=True) | |
# Render an animation to MP4 | |
def render_animation(filepath, engine='BLENDER_EEVEE_NEXT', render=False): | |
# Set render output file path | |
bpy.context.scene.render.filepath = filepath | |
# Set render engine to 'CYCLES' or 'BLENDER_EEVEE_NEXT' | |
bpy.context.scene.render.engine = engine | |
bpy.context.scene.render.use_persistent_data = True # Added to speedup rendering | |
if engine=='CYCLES': | |
bpy.context.scene.cycles.adaptive_threshold = 0.1 # Added to speedup rendering | |
# Set the file format to FFmpeg video | |
bpy.context.scene.render.image_settings.file_format = 'FFMPEG' | |
# Set the codec and encoding options | |
bpy.context.scene.render.ffmpeg.format = 'MPEG4' | |
bpy.context.scene.render.ffmpeg.codec = 'H264' | |
bpy.context.scene.render.ffmpeg.constant_rate_factor = 'HIGH' | |
# Render the animation | |
if render: | |
bpy.ops.render.render(animation=True) | |
# Render an animation with transparency to WEBM | |
def render_transparent_animation(filepath, engine='BLENDER_EEVEE_NEXT'): | |
# Set render output file path | |
bpy.context.scene.render.filepath = filepath | |
# Set render engine to 'CYCLES' or 'BLENDER_EEVEE_NEXT' | |
bpy.context.scene.render.engine = engine | |
bpy.context.scene.render.use_persistent_data = True # Added to speedup rendering | |
if engine=='CYCLES': | |
bpy.context.scene.cycles.adaptive_threshold = 0.1 # Added to speedup rendering | |
# Enable transparency for the world background | |
bpy.context.scene.render.film_transparent = True | |
# Set the file format to FFmpeg video | |
bpy.context.scene.render.image_settings.file_format = 'FFMPEG' | |
# Set the codec and encoding options | |
bpy.context.scene.render.ffmpeg.format = 'WEBM' # or 'QUICKTIME' for .mov | |
bpy.context.scene.render.ffmpeg.codec = 'VP9' # 'WEBM' or 'QTRLE' for QuickTime | |
# Ensure the color mode is RGBA (for transparency) | |
bpy.context.scene.render.image_settings.color_mode = 'RGBA' | |
# Render the animation | |
bpy.ops.render.render(animation=True) | |
# loading brain data | |
################################################################################ | |
################################################################################ | |
( | |
left_vertices_loaded, | |
left_triangles_loaded, | |
right_vertices_loaded, | |
right_triangles_loaded | |
) = load_cortical_meshes('inflated') | |
# Brain scaling (original is mm coordinates) | |
brain_scale = 50 | |
left_vertices_loaded /= (1000 / brain_scale) | |
right_vertices_loaded /= (1000 / brain_scale) | |
dscalar = nib.load(cbu.cifti_template_file) | |
brain_models = [x for x in dscalar.header.get_index_map(1).brain_models] | |
left_cortical_surface_model, right_cortical_surface_model = brain_models[0], brain_models[1] | |
gradients = nib.load("/mnt/local_storage/Research/Codes/fMRI/DataStore/Templates/principal_gradient/hcp.gradients.dscalar.nii") | |
# read subcortex atlas files | |
subcortical_mesh_infos = {} | |
subcortical_atlas_infos = {} | |
msa_scales = ['1', '2', '3', '4'] | |
for msa_scale in msa_scales: | |
msa = nib.load(f"/mnt/local_storage/Research/Codes/fMRI/DataStore/Templates/MSA/Tian_Subcortex_S{msa_scale}_3T_1mm.nii.gz") | |
msa_info = read_msa_atlas_information(f"/mnt/local_storage/Research/Codes/fMRI/DataStore/Templates/MSA/Schaefer2018_100Parcels_7Networks_order_Tian_Subcortex_S{msa_scale}_label.txt") | |
subcortical_atlas_infos[msa_scale] = msa_info | |
subcortical_mesh_infos[msa_scale] = {} | |
for index in np.unique(msa.get_fdata()): | |
if index != 0: | |
voxels_ijk = np.array(np.where(msa.get_fdata() == index)).T | |
transformation_matrix = msa.affine | |
surface_vertices, surface_triangles = cbu.generate_surface_marching_cube( | |
voxels_ijk, transformation_matrix, smoothing_filter="laplacian", | |
simplify=False, smoothing=0, subdivide=False, | |
) | |
surface_vertices /= (1000 / brain_scale) | |
subcortical_mesh_infos[msa_scale][index] = (surface_vertices, surface_triangles) | |
# Blender scripting starts here | |
################################################################################ | |
################################################################################ | |
# First clean the scene | |
blender_clean_up() | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Frame timing | |
start_frame = 1 | |
end_frame = 600 | |
frame_setup(start_frame=start_frame, end_frame=end_frame) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Now create the mesh | |
base_offset = (left_vertices_loaded.mean(0) + right_vertices_loaded.mean(0))/2 | |
left_cortex_mesh, left_cortex_obj = create_mesh_object( | |
left_vertices_loaded - base_offset, left_triangles_loaded, | |
mesh_name="left_cortex_mesh", object_name="left_cortex_obj" | |
) | |
right_cortex_mesh, right_cortex_obj = create_mesh_object( | |
right_vertices_loaded - base_offset, right_triangles_loaded, | |
mesh_name="right_cortex_mesh", object_name="right_cortex_obj" | |
) | |
# and the subcortical regions | |
subcortical_meshes = {} | |
subcortical_objs = {} | |
for msa_scale in msa_scales: | |
msa_info = subcortical_atlas_infos[msa_scale] | |
for index in subcortical_mesh_infos[msa_scale]: | |
subcortical_meshes[f"msa_s{msa_scale}_p{index}"], subcortical_objs[f"msa_s{msa_scale}_p{index}"] = create_mesh_object( | |
subcortical_mesh_infos[msa_scale][index][0] - base_offset, | |
subcortical_mesh_infos[msa_scale][index][1], | |
mesh_name=f"msa_s{msa_scale}_p{index}_{msa_info[index]['name']}_mesh", | |
object_name=f"msa_s{msa_scale}_p{index}_{msa_info[index]['name']}_obj" | |
) | |
# smooth mesh | |
add_corrective_smooth_modifier(subcortical_objs[f"msa_s{msa_scale}_p{index}"]) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Apply smooth shading | |
shade_smooth(left_cortex_obj) | |
shade_smooth(right_cortex_obj) | |
for key in subcortical_objs: | |
shade_smooth(subcortical_objs[key]) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# handling colors | |
# A simple glass material | |
glass_material = create_glass_material() | |
# Make the brain look more real | |
assign_material_to_object(glass_material, left_cortex_obj) | |
assign_material_to_object(glass_material, right_cortex_obj) | |
# Make the glass thin to avoid strange transmissions | |
add_solidify_modifier(left_cortex_obj, thickness=0.0001) | |
add_solidify_modifier(right_cortex_obj, thickness=0.0001) | |
# colored subcortex material | |
sc_materials = {} | |
for msa_scale in msa_scales: | |
msa_info = subcortical_atlas_infos[msa_scale] | |
for index in subcortical_mesh_infos[msa_scale]: | |
sc_materials[f"msa_s{msa_scale}_p{index}"] = create_colored_light_material( | |
color=(msa_info[index]['r'], msa_info[index]['g'], msa_info[index]['b'],1), | |
name=f"msa_s{msa_scale}_p{index}_{msa_info[index]['name']}_material", emission_strength=0.1) | |
assign_material_to_object(sc_materials[f"msa_s{msa_scale}_p{index}"], subcortical_objs[f"msa_s{msa_scale}_p{index}"]) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# handle parents and groups | |
cortex_object = create_data_object("cortex", None) | |
cortex_object.location = (0, 0, 0) | |
left_cortex_obj.parent = cortex_object | |
right_cortex_obj.parent = cortex_object | |
msa_objects = {} | |
for msa_scale in msa_scales: | |
msa_objects[msa_scale] = create_data_object(f"msa_s{msa_scale}", None) | |
msa_objects[msa_scale].location = (0, 0, 0) | |
for index in subcortical_mesh_infos[msa_scale]: | |
subcortical_objs[f"msa_s{msa_scale}_p{index}"].parent = msa_objects[msa_scale] | |
# Add a fixed pivot to move all brain parts together | |
pivot_object = create_data_object("BrainPivot", None) | |
pivot_object.location = (0, 0, 0) | |
# pivot_object.location = (left_vertices_loaded.mean(0) + right_vertices_loaded.mean(0))/2 | |
# parent to pivot to aid brain rotations | |
cortex_object.parent = pivot_object | |
for msa_scale in msa_scales: | |
msa_objects[msa_scale].parent = pivot_object | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Add texts on top of view | |
font_path = "/home/sina/.fonts/Ubuntu Bold Italic Nerd Font Complete.ttf" | |
colors = plt.cm.get_cmap('rainbow', len(msa_scales)) | |
font_colors = [colors(i) for i in range(len(msa_scales))] | |
msa_texts = {} | |
msa_text_materials = {} | |
for i, msa_scale in enumerate(msa_scales): | |
msa_text_materials[msa_scale] = create_colored_material( | |
color=font_colors[i], name="text_color_material" | |
) | |
msa_texts[msa_scale] = create_text_object( | |
name=f"msa_s{msa_scale}_text", location=(0, 0, 40), | |
rotation=(math.radians(90), 0, math.radians(-90)), | |
text=f"MSA: scale {msa_scale}", align_x="CENTER", | |
align_y="TOP", font=font_path, | |
) | |
add_solidify_modifier(msa_texts[msa_scale], thickness=0.1) | |
assign_material_to_object(msa_text_materials[msa_scale], msa_texts[msa_scale]) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Create a wall behind view | |
wall = create_plane( | |
name="WallPlane", scale=(50, 50), | |
location=( | |
left_vertices_loaded[:,0].max() + 20, | |
left_vertices_loaded[:,1].mean(), | |
left_vertices_loaded[:,2].mean() | |
), rotation=(0,math.radians(90),0), subdivision_kws={'subdivide': True, 'cuts': 20} | |
) | |
wall_material = create_colored_material(color=(0.,0.,0.,1), name="wall_material") | |
assign_material_to_object(wall_material, wall) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Add a camera object | |
camera_object, target_object = create_camera( | |
camera_location=brain_scale * np.array((-0.3, 0, 0)), | |
target_location=(0,0,0) | |
# target_location=(left_vertices_loaded.mean(0) + right_vertices_loaded.mean(0))/2 | |
) | |
# camera animation | |
for i in range(end_frame): | |
# Keyframe the rotation of the empty (animate around the Z axis) | |
target_object.rotation_euler = ( | |
0, | |
np.cos(2 * np.pi * i / end_frame)**3 * math.radians(4), | |
np.sin(4 * np.pi * i / end_frame) * math.radians(2) | |
) | |
target_object.keyframe_insert(data_path="rotation_euler", frame=i + 1) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# # add multiple surrounding light sources | |
light_count = 6 | |
# for i in range(light_count + 1): | |
# # Add a new sun light to the scene | |
# create_sunlight( | |
# name=f"SunLight_{i}", | |
# rotation=(np.pi / 2, 0, (-np.pi * (i / light_count)) - (np.pi/2)), | |
# strength=1.0 | |
# ) | |
# and a light source from above | |
create_sunlight( | |
name=f"SunLight_{light_count + 1}", | |
rotation=(0, 0, 0), | |
strength=2.0 | |
) | |
################################################################################ | |
# Function to appear and disappear by animation | |
def appearance_animation(material, appear_start, appear_end, disappear_start, disappear_end): | |
bsdf = material.node_tree.nodes["Principled BSDF"] | |
bsdf.inputs["Alpha"].default_value = 0 | |
bsdf.inputs["Alpha"].keyframe_insert(data_path='default_value', frame=appear_start) | |
bsdf.inputs["Alpha"].default_value = 1 | |
bsdf.inputs["Alpha"].keyframe_insert(data_path='default_value', frame=appear_end) | |
bsdf.inputs["Alpha"].default_value = 1 | |
bsdf.inputs["Alpha"].keyframe_insert(data_path='default_value', frame=disappear_start) | |
bsdf.inputs["Alpha"].default_value = 0 | |
bsdf.inputs["Alpha"].keyframe_insert(data_path='default_value', frame=disappear_end) | |
# Function to bounce text in | |
def obj_appearance_animation(obj, loc0, loc1, appear_start, appear_end): | |
obj.location = loc0 | |
obj.keyframe_insert(data_path='location', frame=appear_start - 1) | |
obj.location = loc1 | |
obj.keyframe_insert(data_path='location', frame=appear_start) | |
obj.location = loc1 | |
obj.keyframe_insert(data_path='location', frame=appear_end) | |
obj.location = loc0 | |
obj.keyframe_insert(data_path='location', frame=appear_end + 1) | |
################################################################################ | |
################################################################################ | |
# Animation directives | |
# brain rotations | |
# Start rotation | |
pivot_object.rotation_euler = (0., 0., 0.) | |
pivot_object.keyframe_insert(data_path="rotation_euler", frame=start_frame) | |
# End rotation | |
pivot_object.rotation_euler = (0., 0., 8*np.pi) | |
pivot_object.keyframe_insert(data_path="rotation_euler", frame=end_frame) | |
# Atlas animations | |
for i, msa_scale in enumerate(msa_scales): | |
t = end_frame // len(msa_scales) | |
times = (t * i) + 5, (t * i) + 15, (t * i) + t - 15, (t * i) + t - 5 | |
appearance_animation(msa_text_materials[msa_scale], *times) | |
obj_appearance_animation(msa_texts[msa_scale], (0, 0, 40), (0, 0, 4), (t * i), (t * i) + t) | |
obj_appearance_animation(msa_objects[msa_scale], (0, 0, -40), (0, 0, 0), (t * i), (t * i) + t) | |
for index in subcortical_mesh_infos[msa_scale]: | |
appearance_animation(sc_materials[f"msa_s{msa_scale}_p{index}"], *times) | |
################################################################################ | |
print("here") | |
################################################################################ | |
################################################################################ | |
# Set the world background color | |
set_background(background_color=(1, 1, 1, 1), ambient_light_strength=1.2) | |
# Set the world background from hdri | |
# hdr_image_path = "/mnt/local_storage/Research/Codes/fMRI/DataStore/Templates/HDRI/evening_road_01_puresky_8k.hdr" | |
# set_hdri_background(hdr_image_path) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# Set render resolution | |
set_render_properties(resolution_x=1000, resolution_y=800, fps=30) | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# render a single frame and store as image | |
# render_single_frame(frame=1, filepath="/mnt/local_storage/Research/Codes/fMRI/DataStore/tmp/blender/rendered_image.png", engine='BLENDER_EEVEE_NEXT') | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# render animation | |
render_animation(filepath="/mnt/local_storage/Research/Codes/fMRI/DataStore/tmp/blender/glass_brain_msa.mp4", engine='BLENDER_EEVEE_NEXT') | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# bpy.ops.screen.animation_play() | |
################################################################################ | |
################################################################################ | |
################################################################################ | |
# # Finally save the project | |
# # Specify the file path where you want to save the Blender project | |
# file_path = "/mnt/local_storage/Research/Codes/fMRI/DataStore/tmp/blender/project_cloth.blend" | |
# # Save the current Blender file | |
# bpy.ops.wm.save_as_mainfile(filepath=file_path) | |
################################################################################ | |
bpy.context.scene.render.engine = 'CYCLES' | |
# bpy.context.space_data.shading.type = 'RENDERED' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is the resulting render!
glass_brain_msa_final.mp4