Gsd-skill-creator creative-pipeline

Generates Blender Python scripts for infrastructure visualization and ffmpeg commands for video production and social media export.

install
source · Clone the upstream repo
git clone https://github.com/Tibsfox/gsd-skill-creator
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Tibsfox/gsd-skill-creator "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/physical-infrastructure/skills/creative-pipeline" ~/.claude/skills/tibsfox-gsd-skill-creator-creative-pipeline && rm -rf "$T"
manifest: skills/physical-infrastructure/skills/creative-pipeline/SKILL.md
source content

Creative Pipeline Skill

At a Glance

Generate Blender Python (bpy) scripts for 3D infrastructure visualization and ffmpeg command sequences for video production and social media export — turning technical engineering designs into communication-ready visual deliverables.

Activation: After engineering design is verified and documented. InfrastructureRequest with outputFormat including 'render'. Stakeholder presentations, client deliverables, social media content creation.

Output types:

  1. Blender bpy scripts: Python scripts that run in Blender's Scripting workspace to create pipe geometry, equipment meshes, materials, lighting, camera paths, and animations.
  2. ffmpeg commands: Shell commands that assemble rendered frames into video and export to social media formats (YouTube, Instagram Reel, Twitter/X, thumbnail).

Prerequisites:

  • Blender 3.6+ installed (free from blender.org)
  • ffmpeg installed and in PATH (free from ffmpeg.org)
  • Completed engineering design with dimensions and layout from blueprint engine

Quick-start — Blender script generation:

import bpy
# Clear scene, set units to metric, configure Cycles renderer
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.context.scene.unit_settings.system = 'METRIC'
bpy.context.scene.render.engine = 'CYCLES'

Quick-start — ffmpeg social media export:

# Assemble rendered frames into video
ffmpeg -framerate 30 -i render_%04d.png -c:v libx264 -pix_fmt yuv420p walkthrough.mp4
# Export for YouTube (1920x1080, 8 Mbps)
ffmpeg -i walkthrough.mp4 -vf "scale=1920:1080" -b:v 8M youtube.mp4

Quick routing:


Blender Scene Generation (CREAT-01)

Scene Setup and Initialization

Every infrastructure visualization script begins with scene initialization:

import bpy
import math

# ── Clear default scene ──────────────────────────────────────
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()

# ── Set units to metric (millimeters for infrastructure) ─────
bpy.context.scene.unit_settings.system = 'METRIC'
bpy.context.scene.unit_settings.length_unit = 'MILLIMETERS'

# ── Set render engine ────────────────────────────────────────
# EEVEE for fast preview, Cycles for photorealistic final render
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 128  # Preview: 128, Final: 256-512

# ── Set render resolution ────────────────────────────────────
bpy.context.scene.render.resolution_x = 1920
bpy.context.scene.render.resolution_y = 1080
bpy.context.scene.render.resolution_percentage = 100

# ── Set output format ────────────────────────────────────────
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.filepath = '//renders/'  # Relative to .blend file

Lighting Setup

Three standard lighting configurations for infrastructure visualization:

Studio lighting (3-point):

# Key light — main illumination at 45 degrees
bpy.ops.object.light_add(type='AREA', location=(5, -5, 8))
key_light = bpy.context.active_object
key_light.name = "Key_Light"
key_light.data.energy = 1000  # Watts
key_light.data.size = 3.0  # Soft shadow (larger = softer)

# Fill light — reduce shadow harshness, opposite side at lower intensity
bpy.ops.object.light_add(type='AREA', location=(-4, -3, 5))
fill_light = bpy.context.active_object
fill_light.name = "Fill_Light"
fill_light.data.energy = 400  # 40% of key light

# Rim light — edge definition from behind
bpy.ops.object.light_add(type='AREA', location=(0, 6, 6))
rim_light = bpy.context.active_object
rim_light.name = "Rim_Light"
rim_light.data.energy = 600

HDRI environment (realistic ambient):

# Load HDRI for realistic outdoor/sky lighting
world = bpy.context.scene.world
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()

bg_node = nodes.new(type='ShaderNodeBackground')
env_node = nodes.new(type='ShaderNodeTexEnvironment')
output_node = nodes.new(type='ShaderNodeOutputWorld')

# Load HDRI file (download from polyhaven.com — free CC0)
env_node.image = bpy.data.images.load('//hdri/industrial_hall.hdr')
bg_node.inputs['Strength'].default_value = 1.0

links.new(env_node.outputs['Color'], bg_node.inputs['Color'])
links.new(bg_node.outputs['Background'], output_node.inputs['Surface'])

Volumetric (for coolant flow visualization with god rays):

# Add volumetric scatter to world for atmospheric depth
vol_node = nodes.new(type='ShaderNodeVolumePrincipled')
vol_node.inputs['Density'].default_value = 0.01  # Subtle atmosphere
vol_node.inputs['Anisotropy'].default_value = 0.3  # Forward scattering
links.new(vol_node.outputs['Volume'], output_node.inputs['Volume'])

Pipe Routing as Bezier Curves (CREAT-02)

Creating Pipe Geometry

Pipes are modeled as Bezier curves with a circular bevel profile. The bevel_depth sets the pipe outer radius.

# Create pipe as Bezier curve with circular cross-section
bpy.ops.curve.primitive_bezier_curve_add()
curve = bpy.context.active_object
curve.name = "CoolingPipe_Supply"

# Configure curve as 3D pipe
curve_data = curve.data
curve_data.dimensions = '3D'
curve_data.bevel_depth = 0.0445  # Outer radius in meters (3" NPS = 88.9mm OD)
curve_data.bevel_resolution = 8  # Smoothness of circular cross-section
curve_data.fill_mode = 'FULL'

# Set pipe route using spline control points
spline = curve_data.splines[0]
spline.bezier_points[0].co = (0, 0, 3.0)       # Start point (meters)
spline.bezier_points[1].co = (5.0, 0, 3.0)     # End point (meters)

# Handle positions control curve direction (tangent at each point)
spline.bezier_points[0].handle_left = (-0.5, 0, 3.0)
spline.bezier_points[0].handle_right = (1.0, 0, 3.0)
spline.bezier_points[1].handle_left = (4.0, 0, 3.0)
spline.bezier_points[1].handle_right = (5.5, 0, 3.0)

Adding Direction Changes (Elbows)

For pipe direction changes, add intermediate Bezier points with handle angles:

# Add a point for a 90-degree direction change (elbow)
spline.bezier_points.add(1)  # Now 3 points total

# Route: horizontal run -> 90-deg down -> vertical drop
spline.bezier_points[0].co = (0, 0, 3.0)        # Horizontal start
spline.bezier_points[1].co = (4.0, 0, 3.0)      # Corner (elbow location)
spline.bezier_points[2].co = (4.0, 0, 0.5)      # Vertical end (drops to 0.5m)

# Set handles at corner for sharp 90-degree turn
spline.bezier_points[1].handle_left = (3.5, 0, 3.0)   # Approach from left
spline.bezier_points[1].handle_right = (4.0, 0, 2.5)  # Exit downward

# Handle type: AUTO for smooth curves, FREE for precise control
for point in spline.bezier_points:
    point.handle_left_type = 'FREE'
    point.handle_right_type = 'FREE'

NPS to Blender bevel_depth Conversion Table

The bevel_depth parameter is the pipe outer radius in meters (OD / 2):

NPS (inch)OD (mm)OD (m)bevel_depth (m)Common Use
1/2"21.30.02130.0107Small branch lines
3/4"26.70.02670.0134Residential supply
1"33.40.03340.0167Branch distribution
1-1/2"48.30.04830.0242Medium branch lines
2"60.30.06030.0302CDU branch connections
3"88.90.08890.0445Supply/return headers
4"114.30.11430.0572Main distribution
6"168.30.16830.0842Large distribution
8"219.10.21910.1096Plant piping
10"273.10.27310.1366Large plant piping
12"323.80.32380.1619Municipal/plant mains

Multi-Pipe Routing

For parallel supply/return headers, offset the second pipe by the pipe OD plus insulation thickness:

# Create return pipe offset from supply pipe
offset_y = 0.20  # 200mm center-to-center (3" pipe + insulation + clearance)

bpy.ops.curve.primitive_bezier_curve_add()
return_pipe = bpy.context.active_object
return_pipe.name = "CoolingPipe_Return"
return_pipe.data.dimensions = '3D'
return_pipe.data.bevel_depth = 0.0445  # Same size as supply

spline_return = return_pipe.data.splines[0]
spline_return.bezier_points[0].co = (0, offset_y, 3.0)
spline_return.bezier_points[1].co = (5.0, offset_y, 3.0)

Equipment as Parametric Meshes (CREAT-02)

Creating Equipment from Dimensions

Equipment is modeled as scaled cube primitives positioned at their design locations:

# Create CDU enclosure (600mm x 800mm x 1800mm)
bpy.ops.mesh.primitive_cube_add(location=(2.0, 0, 0.9))  # Center at half-height
cdu = bpy.context.active_object
cdu.name = "CDU_101"
cdu.scale = (0.3, 0.4, 0.9)  # Half-extents in meters (600/2, 800/2, 1800/2)
bpy.ops.object.transform_apply(scale=True)  # Apply scale to mesh data

# Create server rack (42U: 600mm x 1000mm x 2000mm)
bpy.ops.mesh.primitive_cube_add(location=(2.0, 2.0, 1.0))
rack = bpy.context.active_object
rack.name = "Rack_A01"
rack.scale = (0.3, 0.5, 1.0)
bpy.ops.object.transform_apply(scale=True)

Standard Equipment Dimensions for Data Center

Use these dimensions for parametric mesh generation:

EquipmentW x D x H (mm)Blender scale (x, y, z)Notes
Server rack (42U)600 x 1000 x 2000(0.3, 0.5, 1.0)Standard EIA-310-E
Server rack (48U)600 x 1200 x 2200(0.3, 0.6, 1.1)Deep rack for GPU servers
CDU (small)600 x 800 x 1800(0.3, 0.4, 0.9)30-60 kW range
CDU (large)800 x 1200 x 2000(0.4, 0.6, 1.0)100+ kW range
UPS (100 kVA)800 x 1000 x 1900(0.4, 0.5, 0.95)Floor-standing
UPS (500 kVA)1200 x 1000 x 2000(0.6, 0.5, 1.0)Large frame
Floor PDU600 x 800 x 1800(0.3, 0.4, 0.9)With transformer
Pump (centrifugal)500 x 800 x 600(0.25, 0.4, 0.3)Horizontal base-mounted
Panel board400 x 200 x 600(0.2, 0.1, 0.3)Wall-mounted
Transformer (dry)1000 x 800 x 1200(0.5, 0.4, 0.6)500-1000 kVA
Cable tray (section)300 x 100 x 3000(0.15, 0.05, 1.5)12" ladder type

Equipment Detail — Louvers and Airflow Indicators

Add visual detail to equipment faces to indicate airflow direction:

# Add edge loops to front face for louver appearance
bpy.context.view_layer.objects.active = cdu
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.loopcut_slide(
    MESH_OT_loopcut={'number_cuts': 8, 'smoothness': 0},
    TRANSFORM_OT_edge_slide={'value': 0}
)
bpy.ops.object.mode_set(mode='OBJECT')

Infrastructure Materials (CREAT-02)

Material Overview

Seven PBR (Physically Based Rendering) materials cover common infrastructure components. All use the Principled BSDF shader node for physically correct rendering.

MaterialBase Color (RGB)MetallicRoughnessSpecialEngineering Use
Copper pipe(0.83, 0.48, 0.22)1.00.2ASTM B88 tubing, heat exchangers
PVC pipe(0.9, 0.9, 0.88)0.00.85DWV piping, conduit
Brushed steel(0.6, 0.6, 0.62)1.00.3ASTM A53 pipe, structural steel
Insulation(0.72, 0.62, 0.35)0.01.0Fiberglass/mineral wool jacket
Coolant(0.2, 0.5, 1.0)0.00.0IOR=1.33, transmission=0.8Chilled water, glycol visualization
Concrete(0.5, 0.5, 0.5)0.00.95Slab, walls, raised floor
Cable tray(0.75, 0.75, 0.72)0.80.4Galvanized steel per NEMA VE 1

Material Assignment

# Assign material to pipe object
mat = bpy.data.materials.get("Infra_CopperPipe")
if mat is None:
    # Create material if not already loaded from blender-materials.py
    mat = bpy.data.materials.new(name="Infra_CopperPipe")
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get('Principled BSDF')
    bsdf.inputs['Base Color'].default_value = (0.83, 0.48, 0.22, 1.0)
    bsdf.inputs['Metallic'].default_value = 1.0
    bsdf.inputs['Roughness'].default_value = 0.2

# Assign to object
pipe_obj = bpy.data.objects['CoolingPipe_Supply']
if pipe_obj.data.materials:
    pipe_obj.data.materials[0] = mat
else:
    pipe_obj.data.materials.append(mat)

Full material setup scripts: All 7 materials with complete Principled BSDF configuration, helper functions, and engineering context are available in

@references/blender-materials.py
. Paste the entire script into Blender's Scripting workspace and run to create all materials at once.


Camera Paths (CREAT-03)

Orbital Overview Camera

Camera orbits the scene center on a circular path — best for showing overall system layout:

import math

# Create camera
bpy.ops.object.camera_add(location=(0, -10, 5))
camera = bpy.context.active_object
camera.name = "Camera_Orbital"
bpy.context.scene.camera = camera  # Set as active camera

# Create circular orbit path
bpy.ops.curve.primitive_bezier_circle_add(radius=10, location=(0, 0, 3))
orbit_path = bpy.context.active_object
orbit_path.name = "Camera_Orbit_Path"

# Add Follow Path constraint to camera
constraint = camera.constraints.new(type='FOLLOW_PATH')
constraint.target = orbit_path
constraint.use_fixed_location = False
constraint.forward_axis = 'TRACK_NEGATIVE_Z'
constraint.up_axis = 'UP_Y'

# Add Track To constraint (camera always looks at scene center)
track = camera.constraints.new(type='TRACK_TO')
track.target = bpy.data.objects.get('CDU_101')  # Or create an empty at center
track.track_axis = 'TRACK_NEGATIVE_Z'
track.up_axis = 'UP_Y'

# Set animation timeline
orbit_path.data.path_duration = 250  # 10 seconds at 25 fps
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 250

# Animate path evaluation
orbit_path.data.eval_time = 0
orbit_path.data.keyframe_insert('eval_time', frame=1)
orbit_path.data.eval_time = 250
orbit_path.data.keyframe_insert('eval_time', frame=250)

Linear Walkthrough Camera

Camera moves along a straight path through the facility at eye level — best for showing hot/cold aisle layout:

# Create walkthrough path (straight line through data hall)
bpy.ops.curve.primitive_bezier_curve_add()
walk_path = bpy.context.active_object
walk_path.name = "Camera_Walk_Path"
walk_path.data.dimensions = '3D'

spline = walk_path.data.splines[0]
spline.bezier_points[0].co = (0, -8, 1.7)   # Start: entrance at eye level (1.7m)
spline.bezier_points[1].co = (0, 12, 1.7)   # End: far side of data hall

# Smooth handles for gentle camera motion
spline.bezier_points[0].handle_right = (0, -4, 1.7)
spline.bezier_points[1].handle_left = (0, 8, 1.7)

# Create camera and attach to path
bpy.ops.object.camera_add()
walk_cam = bpy.context.active_object
walk_cam.name = "Camera_Walkthrough"

constraint = walk_cam.constraints.new(type='FOLLOW_PATH')
constraint.target = walk_path
constraint.forward_axis = 'FORWARD_Y'
constraint.up_axis = 'UP_Z'

# Animate over 15 seconds (375 frames at 25 fps)
walk_path.data.path_duration = 375
bpy.context.scene.frame_end = 375
walk_path.data.eval_time = 0
walk_path.data.keyframe_insert('eval_time', frame=1)
walk_path.data.eval_time = 375
walk_path.data.keyframe_insert('eval_time', frame=375)

Detail Zoom Camera

Camera starts at overview distance and animates to focus on a specific component — best for highlighting equipment:

# Create zoom path from far to close
bpy.ops.curve.primitive_bezier_curve_add()
zoom_path = bpy.context.active_object
zoom_path.name = "Camera_Zoom_Path"
zoom_path.data.dimensions = '3D'

target_pos = (2.0, 0, 0.9)  # CDU center position

spline = zoom_path.data.splines[0]
spline.bezier_points[0].co = (10, -8, 6)      # Start: overview (far, high)
spline.bezier_points[1].co = (3.0, -1.5, 1.2) # End: detail (close, eye level)

# Create camera with Track To constraint aimed at equipment
bpy.ops.object.camera_add()
zoom_cam = bpy.context.active_object
zoom_cam.name = "Camera_DetailZoom"

path_constraint = zoom_cam.constraints.new(type='FOLLOW_PATH')
path_constraint.target = zoom_path

track_constraint = zoom_cam.constraints.new(type='TRACK_TO')
target_empty = bpy.data.objects.new("Zoom_Target", None)
bpy.context.collection.objects.link(target_empty)
target_empty.location = target_pos
track_constraint.target = target_empty
track_constraint.track_axis = 'TRACK_NEGATIVE_Z'
track_constraint.up_axis = 'UP_Y'

# Animate over 5 seconds with ease-in/ease-out
zoom_path.data.path_duration = 125  # 5 sec at 25 fps
bpy.context.scene.frame_end = 125

Camera Path Summary

Path TypeUse CaseDurationCamera HeightOrbit Radius
Orbital overviewFull system layout8-12 sec3-5m above center10-20m (scene dependent)
Linear walkthroughHot/cold aisle tour12-20 sec1.7m (eye level)N/A (straight line)
Detail zoomEquipment spotlight4-8 secStarts high, ends eye-levelN/A (linear approach)

Infrastructure Animations (CREAT-01, CREAT-03)

Coolant Flow Animation (Particle System)

Particles follow the pipe curve path to visualize fluid flow:

# Get the supply pipe curve object
pipe_obj = bpy.data.objects['CoolingPipe_Supply']

# Convert curve to mesh temporarily for particle emission
# (Particles emit from mesh faces, not curve surfaces)
bpy.context.view_layer.objects.active = pipe_obj
bpy.ops.object.convert(target='MESH', keep_original=True)
pipe_mesh = bpy.context.active_object
pipe_mesh.name = "CoolingPipe_Supply_Mesh"

# Create small sphere for coolant droplet visualization
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.01, location=(0, 0, -10))
droplet = bpy.context.active_object
droplet.name = "Coolant_Droplet"

# Assign coolant material to droplet
coolant_mat = bpy.data.materials.get("Infra_Coolant")
if coolant_mat:
    droplet.data.materials.append(coolant_mat)

# Add particle system to pipe mesh
bpy.context.view_layer.objects.active = pipe_mesh
bpy.ops.object.particle_system_add()
ps = pipe_mesh.particle_systems[0]
ps_settings = ps.settings

ps_settings.count = 200           # Number of particles (more = denser flow)
ps_settings.lifetime = 80         # Frames each particle lives
ps_settings.emit_from = 'FACE'    # Emit from pipe surface
ps_settings.render_type = 'OBJECT'
ps_settings.instance_object = droplet
ps_settings.particle_size = 1.0
ps_settings.size_random = 0.2     # Slight size variation for realism

# Set velocity along pipe normal (simulates flow direction)
ps_settings.normal_factor = 0.5   # Speed along surface normal
ps_settings.factor_random = 0.1   # Slight randomness

Electrical Current Animation (Emissive Pulse)

Animate emission strength along a conductor to simulate current flow:

# Create or get copper conductor material with emission node
mat = bpy.data.materials.new(name="Copper_Conductor_Animated")
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()

# Principled BSDF for base appearance
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (600, 0)

bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.location = (0, 0)
bsdf.inputs['Base Color'].default_value = (0.83, 0.48, 0.22, 1.0)
bsdf.inputs['Metallic'].default_value = 1.0
bsdf.inputs['Roughness'].default_value = 0.2

# Add emission for glow effect
bsdf.inputs['Emission Color'].default_value = (1.0, 0.8, 0.2, 1.0)  # Warm yellow
bsdf.inputs['Emission Strength'].default_value = 0

links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

# Animate emission strength: off -> bright -> off (pulse effect)
bpy.context.scene.frame_set(1)
bsdf.inputs['Emission Strength'].default_value = 0
bsdf.inputs['Emission Strength'].keyframe_insert('default_value', frame=1)

bpy.context.scene.frame_set(15)
bsdf.inputs['Emission Strength'].default_value = 10  # Peak brightness
bsdf.inputs['Emission Strength'].keyframe_insert('default_value', frame=15)

bpy.context.scene.frame_set(30)
bsdf.inputs['Emission Strength'].default_value = 0
bsdf.inputs['Emission Strength'].keyframe_insert('default_value', frame=30)

# For repeating pulse: set keyframe extrapolation to cycle
# (requires Graph Editor > Channel > Extrapolation Mode > Make Cyclic)

Thermal Mapping Animation (Color Ramp from Temperature Data)

Color-code surfaces by temperature using a custom attribute driven by calculation data:

# Create thermal visualization material
mat_thermal = bpy.data.materials.new(name="Thermal_Mapping")
mat_thermal.use_nodes = True
nodes = mat_thermal.node_tree.nodes
links = mat_thermal.node_tree.links
nodes.clear()

output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (800, 0)

bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.location = (400, 0)

# Color ramp: blue (cold) -> green -> yellow -> red (hot)
color_ramp = nodes.new(type='ShaderNodeValToRGB')
color_ramp.location = (0, 0)
color_ramp.color_ramp.elements[0].position = 0.0
color_ramp.color_ramp.elements[0].color = (0.0, 0.0, 1.0, 1.0)  # Blue (cold)
color_ramp.color_ramp.elements.new(0.33)
color_ramp.color_ramp.elements[1].color = (0.0, 1.0, 0.0, 1.0)  # Green
color_ramp.color_ramp.elements.new(0.66)
color_ramp.color_ramp.elements[2].color = (1.0, 1.0, 0.0, 1.0)  # Yellow
color_ramp.color_ramp.elements[3].position = 1.0
color_ramp.color_ramp.elements[3].color = (1.0, 0.0, 0.0, 1.0)  # Red (hot)

# Attribute node to read temperature values from mesh custom attribute
attr_node = nodes.new(type='ShaderNodeAttribute')
attr_node.location = (-200, 0)
attr_node.attribute_name = "temperature"  # Custom per-vertex attribute

links.new(attr_node.outputs['Fac'], color_ramp.inputs['Fac'])
links.new(color_ramp.outputs['Color'], bsdf.inputs['Base Color'])
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

# To apply temperature data to vertices:
# mesh.attributes.new(name='temperature', type='FLOAT', domain='POINT')
# mesh.attributes['temperature'].data[vertex_index].value = normalized_temp (0-1)

Animation Summary

AnimationTechniqueVisual EffectFrame Count
Coolant flowParticle system on pipe meshBlue spheres flowing through pipes200-500 (8-20s)
Electrical currentEmissive keyframe animationGlowing pulse along conductors60-150 (2-6s per cycle)
Thermal mappingColor ramp + vertex attributeBlue-to-red heat map on surfacesStatic or animated over time

ffmpeg Assembly Pipeline (CREAT-04, CREAT-05)

Step 1: Assemble Rendered Frames into Base Video

After rendering animation frames from Blender (File > Render > Render Animation), assemble into video:

# Assemble PNG frames into H.264 video
# -framerate: must match Blender render settings (25 or 30 fps)
# -crf 18: visually lossless quality (lower = better, 0 = lossless, 23 = default)
ffmpeg -framerate 30 -i render_%04d.png \
    -c:v libx264 \
    -preset slow \
    -crf 18 \
    -pix_fmt yuv420p \
    walkthrough.mp4

Step 2: Add Narration Audio (Optional)

# Combine video with narration audio track
ffmpeg -i walkthrough.mp4 -i narration.mp3 \
    -c:v copy \
    -c:a aac \
    -b:a 192k \
    -shortest \
    final.mp4

Step 3: Social Media Exports

Four export presets for common platforms (full scripts in

@references/ffmpeg-presets.sh
):

YouTube (16:9, 1920x1080, H.264):

ffmpeg -i final.mp4 \
    -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
    -c:v libx264 -preset slow -crf 18 -b:v 8M -maxrate 10M -bufsize 20M \
    -pix_fmt yuv420p -c:a aac -b:a 192k \
    youtube_export.mp4

Instagram Reel (9:16 portrait, 1080x1920):

ffmpeg -i final.mp4 \
    -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black" \
    -c:v libx264 -preset medium -crf 23 -b:v 3.5M \
    -pix_fmt yuv420p -c:a aac -b:a 128k -t 60 \
    instagram_reel.mp4

Twitter/X (16:9, 1280x720, max 2:20):

ffmpeg -i final.mp4 \
    -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2" \
    -c:v libx264 -preset medium -crf 23 -b:v 5M \
    -pix_fmt yuv420p -c:a aac -b:a 128k -t 140 \
    twitter_export.mp4

Thumbnail (JPEG still frame, 1280x720):

# Extract a single frame at 5-second mark
ffmpeg -i final.mp4 \
    -ss 00:00:05 \
    -frames:v 1 \
    -vf "scale=1280:720" \
    -q:v 2 \
    thumbnail.jpg

Social Media Format Summary

PlatformResolutionAspect RatioMax DurationBitrateCodec
YouTube1920x108016:9Unlimited8 MbpsH.264
Instagram Reel1080x19209:1660s (feed) / 90s (reel)3.5 MbpsH.264
Twitter/X1280x72016:92:205 MbpsH.264
Thumbnail1280x72016:9N/A (still)N/AJPEG
LinkedIn1920x108016:910 min8 MbpsH.264

Render Settings Recommendations

Use CaseRender EngineSamplesResolutionEst. Time/FrameNotes
PreviewEEVEEN/A1280x720~1sReal-time preview, good for iteration
DraftCycles641920x1080~15sNoisy but compositionally correct
FinalCycles2561920x1080~60sProduction quality
ThumbnailCycles5122560x1440~120sHigh quality for downsampling

Deep Reference

Complex Multi-Pipe Systems

For systems with many pipes (supply, return, condensate, makeup water), organize by system and color-code:

# System color convention (matching ASME A13.1 pipe identification)
PIPE_SYSTEMS = {
    'chilled_water_supply': {'color': (0.0, 0.5, 1.0, 1.0), 'label': 'CWS'},
    'chilled_water_return': {'color': (0.0, 0.8, 0.3, 1.0), 'label': 'CWR'},
    'condenser_supply':     {'color': (1.0, 0.6, 0.0, 1.0), 'label': 'CS'},
    'condenser_return':     {'color': (1.0, 0.3, 0.0, 1.0), 'label': 'CR'},
    'condensate_drain':     {'color': (0.5, 0.5, 0.5, 1.0), 'label': 'CD'},
}

def create_pipe_material(system_name):
    config = PIPE_SYSTEMS[system_name]
    mat = bpy.data.materials.new(name=f"Pipe_{config['label']}")
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get('Principled BSDF')
    bsdf.inputs['Base Color'].default_value = config['color']
    bsdf.inputs['Metallic'].default_value = 0.5
    bsdf.inputs['Roughness'].default_value = 0.4
    return mat

Advanced PBR Materials (Bump Maps and Displacement)

For photorealistic concrete and insulation surfaces, add procedural bump mapping:

# Concrete with procedural bump texture
mat = bpy.data.materials.get("Infra_Concrete")
nodes = mat.node_tree.nodes
links = mat.node_tree.links

# Add noise texture for surface irregularity
noise = nodes.new(type='ShaderNodeTexNoise')
noise.inputs['Scale'].default_value = 50.0
noise.inputs['Detail'].default_value = 6.0
noise.inputs['Roughness'].default_value = 0.6

# Add bump node to convert texture to normal perturbation
bump = nodes.new(type='ShaderNodeBump')
bump.inputs['Strength'].default_value = 0.3  # Subtle bump
bump.inputs['Distance'].default_value = 0.001

bsdf = nodes.get('Principled BSDF')
links.new(noise.outputs['Fac'], bump.inputs['Height'])
links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])

Camera Rig for Automated Pan-and-Scan

For large data halls requiring multiple camera angles:

# Define camera waypoints (x, y, z, look_at_x, look_at_y, look_at_z)
WAYPOINTS = [
    (0, -10, 5, 0, 0, 2),      # Overview from entrance
    (0, 0, 1.7, 3, 0, 1),      # Aisle view looking at rack row
    (3, 0, 1.2, 3, 0, 0.9),    # CDU detail
    (0, 5, 1.7, -3, 5, 1),     # Opposite aisle
    (0, 10, 5, 0, 5, 2),       # Overview from far end
]

# Calculate total frames (5 seconds per segment)
fps = 25
seconds_per_segment = 5
total_frames = len(WAYPOINTS) * fps * seconds_per_segment

ACES Color Management

For physically correct HDR rendering with proper color space:

# Set color management to ACES (if available in Blender build)
bpy.context.scene.display_settings.display_device = 'sRGB'
bpy.context.scene.view_settings.view_transform = 'Filmic'  # Or 'AgX' in Blender 4.x
bpy.context.scene.view_settings.look = 'High Contrast'
bpy.context.scene.view_settings.exposure = 0.0
bpy.context.scene.view_settings.gamma = 1.0

Geometry Nodes for Parametric Rack Arrays

For generating rows of identical racks automatically:

# Instance racks along a line using Array modifier
rack_template = bpy.data.objects['Rack_A01']
array_mod = rack_template.modifiers.new(name="Row_Array", type='ARRAY')
array_mod.count = 10  # 10 racks per row
array_mod.relative_offset_displace = (0, 1.2, 0)  # 1.2m pitch (600mm rack + 600mm aisle)
array_mod.use_relative_offset = True

Batch Rendering Script

For rendering all camera angles and animation sequences:

# Render all camera paths sequentially
cameras = ['Camera_Orbital', 'Camera_Walkthrough', 'Camera_DetailZoom']

for cam_name in cameras:
    camera = bpy.data.objects.get(cam_name)
    if camera:
        bpy.context.scene.camera = camera
        bpy.context.scene.render.filepath = f'//renders/{cam_name}/'
        bpy.ops.render.render(animation=True)
        print(f"Rendered: {cam_name}")

Output Passes for Compositing

Configure render passes for post-processing and compositing:

# Enable useful render passes
view_layer = bpy.context.scene.view_layers[0]
view_layer.use_pass_diffuse_color = True
view_layer.use_pass_glossy_color = True
view_layer.use_pass_emit = True
view_layer.use_pass_ambient_occlusion = True
view_layer.use_pass_z = True  # Depth pass for DOF in compositing

# Enable denoising (Cycles only)
bpy.context.scene.cycles.use_denoising = True
bpy.context.scene.cycles.denoiser = 'OPENIMAGEDENOISE'

Reference Files

ReferencePurposeUsage
@references/blender-materials.pyAll 7 infrastructure materials as ready-to-paste bpy scriptPaste into Blender Scripting workspace and run
@references/ffmpeg-presets.shAll 4 social media export commands as bash functionsSource the file or copy individual commands

Creative Pipeline Skill v1.0.0 — Physical Infrastructure Engineering Pack Phase 440-02 | References: Blender bpy API, ffmpeg, ASME A13.1 (pipe identification) This skill generates visualization scripts only — no engineering decisions are made.