Search

Using Blender for Scientific Visualization

Result: result Script (Blender 3.6 LTS):

import bpy
import math
from mathutils import Vector

###############################
# Step 1: Clean up all objects
###############################
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

###########################
# Step 2: Main code (Josephson chain setup)
###########################

# Parameters for the unit cell
unit_length = 1.0  # Adjust the size as needed
length_ratio = 15
width_ratio = 6
height_ratio = 1
num_cells = 50         # Number of unit cells to duplicate in each layer
y_interval = 4         # Spacing between each unit cell along the Y-axis
layer_offset_y = 2     # Offset for the second layer in the Y-axis
x_offset = 30          # Spacing between each duplicated chain along the X-axis
num_chains = 2         # Number of chains to duplicate along the X-axis

# Calculate dimensions
unit_cell_length = unit_length * length_ratio
unit_cell_width  = unit_length * width_ratio
unit_cell_height = unit_length * height_ratio

# Create a single unit cell as the basis
bpy.ops.mesh.primitive_cube_add(size=1)
unit_cell = bpy.context.object
unit_cell.scale = (unit_cell_length / 2, unit_cell_width / 2, unit_cell_height / 2)
unit_cell.location = (0, 0, unit_cell_height / 2)
unit_cell.name = "Unit_Cell"

def create_chain(x_location):
    first_layer = []
    for i in range(num_cells):
        new_cell = unit_cell.copy()
        new_cell.data = unit_cell.data.copy()
        new_cell.location = (x_location, i * y_interval, unit_cell_height / 2)
        bpy.context.collection.objects.link(new_cell)
        first_layer.append(new_cell)

    # Second layer
    for cell in first_layer:
        new_cell = cell.copy()
        new_cell.data = cell.data.copy()
        new_cell.location = (
            cell.location[0],
            cell.location[1] + layer_offset_y,
            cell.location[2] + unit_cell_height / 2
        )
        bpy.context.collection.objects.link(new_cell)

# Create multiple chains
for j in range(num_chains):
    create_chain(x_location=j * x_offset)

# Create a large background plane
bpy.ops.mesh.primitive_plane_add(size=500)
background_plane = bpy.context.object
background_plane.location = (0, 0, 0)
background_plane.rotation_euler = (0, 0, 0)
background_plane.name = "Background_Plane"

# Metallic material functions
def create_realistic_metallic_material(name, base_color, bump_strength=0.1, noise_scale=50.0):
    mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links

    principled = nodes.get("Principled BSDF")
    principled.inputs["Base Color"].default_value = base_color
    principled.inputs["Metallic"].default_value = 0.5
    principled.inputs["Roughness"].default_value = 0.3

    noise = nodes.new(type="ShaderNodeTexNoise")
    noise.inputs["Scale"].default_value = noise_scale
    noise.inputs["Detail"].default_value = 10.0
    noise.inputs["Roughness"].default_value = 0.5

    bump = nodes.new(type="ShaderNodeBump")
    bump.inputs["Strength"].default_value = bump_strength

    ramp = nodes.new(type="ShaderNodeValToRGB")
    ramp.color_ramp.interpolation = 'LINEAR'
    ramp.color_ramp.elements[0].position = 0.4
    ramp.color_ramp.elements[1].position = 0.6

    links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
    links.new(ramp.outputs["Color"], principled.inputs["Roughness"])
    links.new(noise.outputs["Fac"], bump.inputs["Height"])
    links.new(bump.outputs["Normal"], principled.inputs["Normal"])

    return mat

realistic_metal = create_realistic_metallic_material(
    "Realistic_Microchip_Metal", (0.4, 0.4, 0.4, 1), bump_strength=1.3, noise_scale=500.0
)
for obj in bpy.context.collection.objects:
    if "Unit_Cell" in obj.name:
        obj.data.materials.clear()
        obj.data.materials.append(realistic_metal)

background_color = (0.1, 0.1, 0.1, 1)
background_mat = create_realistic_metallic_material(
    "Background_Material", background_color, bump_strength=0.6, noise_scale=10000.0
)
background_plane.data.materials.clear()
background_plane.data.materials.append(background_mat)

# Add a connector block
bpy.ops.mesh.primitive_cube_add(size=1)
connector = bpy.context.object
connector.scale = (x_offset+unit_cell_length/2, unit_cell_width*2/2, unit_cell_height/2)
connector.location = ((num_chains - 1) * x_offset/2, -y_interval/2, unit_cell_height / 2)
connector.name = "Connector"
connector.data.materials.clear()
connector.data.materials.append(realistic_metal)

print("C-shaped connector added.")

# Env lighting
bpy.context.scene.world.use_nodes = True
wnodes = bpy.context.scene.world.node_tree.nodes
for node in wnodes:
    wnodes.remove(node)

bg = wnodes.new(type="ShaderNodeBackground")
bg.inputs["Color"].default_value = (0.3, 0.3, 0.3, 1)
bg.inputs["Strength"].default_value = 0.3
out = wnodes.new(type="ShaderNodeOutputWorld")
bpy.context.scene.world.node_tree.links.new(bg.outputs["Background"], out.inputs["Surface"])

# Sun lamp
bpy.ops.object.light_add(type='SUN', align='WORLD', location=(10, -10, 20))
sun = bpy.context.object
sun.data.energy = 5.0
sun.rotation_euler = (0.785, 0.785, 0)
sun.data.shadow_soft_size = 0.5
sun.data.use_shadow = True

######################
# Step 3: Camera + Animation
######################

bpy.ops.object.camera_add(location=(0,0,0))
camera = bpy.context.object
camera.name = "Main_Camera"
bpy.context.scene.camera = camera

# Set camera properties for better framing
camera.data.lens = 50  # 50mm focal length for a natural perspective
camera.data.dof.use_dof = True
camera.data.dof.focus_distance = 40.0  # Approximate distance to target

fps = 12
duration_seconds = 10
frame_count = fps * duration_seconds
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = frame_count

# Target point that camera will always face
target_x = 15
target_y = 50.0
target_z = 0.0
target = Vector((target_x, target_y, target_z))

radius = 40.0
height = 20.0

for frame in range(1, frame_count + 1):
    bpy.context.scene.frame_set(frame)

    t = (frame - 1) / (frame_count - 1)  # fraction from 0..1
    angle = 2.0 * math.pi * t  # revolve 0..360

    # Calculate camera position
    x = target_x + radius * math.cos(angle)
    y = target_y + radius * math.sin(angle)
    z = target_z + height

    camera.location = (x, y, z)

    # Point camera at target
    direction = target - Vector(camera.location)
    rot_quat = direction.to_track_quat('-Z', 'Y')
    camera.rotation_mode = 'QUATERNION'
    camera.rotation_quaternion = rot_quat

    # Insert keyframes
    camera.keyframe_insert(data_path='location')
    camera.keyframe_insert(data_path='rotation_quaternion')

# Add an Empty at target point for visualization (helpful during setup)
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(target_x, target_y, target_z))
target_empty = bpy.context.object
target_empty.name = "Camera_Target"
target_empty.scale = (2, 2, 2)  # Make it easier to see

################
# Step 4: Output
################

bpy.context.scene.render.filepath = "/Users/jiakai/Desktop/josephson_frames/"
bpy.context.scene.render.image_settings.file_format = 'PNG'

print("Setup complete. Render with 'Render → Render Animation' or:")

bpy.ops.render.render('INVOKE_DEFAULT', animation=True)