Blender APIs
These api's were designed and built for training a foundational Blender AI model.
Blender Foundational AI Model Visualization
Visualize how a foundational model learns in blender
run_ai_training_visualizer(model_file=None, num_gen=None, target_faces=None, target_rows=None, target_cols=None, layer_names=None, max_layers=None, save_gif=None, save_gltf=None, save_stl=None, num_frames=None, output_dir=None, decimation_ratio=None, shutdown=None)
run_ai_training_visualizer
extract a model.safetensors file with mmap (fast) then perform matrix transformations to convert all the 2d array (model weights) to a 3d array for showing in blender using skimage.meaure.marching_cubes
supports saving an animation of flying through the 3d model weights as a gif (note: thie takes a lot of cpu/ram tested on 12 cpu, 23 gb ram)
Parameters: |
|
---|
Raises: |
|
---|
Source code in bw/bl/run_ai_training_visualizer.py
def run_ai_training_visualizer(
model_file: str = None,
num_gen: int = None,
target_faces: int = None,
target_rows: int = None,
target_cols: int = None,
layer_names: str = None,
max_layers: int = None,
save_gif: str = None,
save_gltf: str = None,
save_stl: str = None,
num_frames: int = None,
output_dir: str = None,
decimation_ratio: float = None,
shutdown: bool = None,
):
"""
run_ai_training_visualizer
extract a model.safetensors file with mmap (fast)
then perform matrix transformations to convert all
the 2d array (model weights) to a 3d array
for showing in blender using skimage.meaure.marching_cubes
supports saving an animation of flying through the
3d model weights as a gif (note: thie takes a lot of
cpu/ram tested on 12 cpu, 23 gb ram)
:param model_file: path to model.safetensors file
and uses the MODEL
environment variable
(e.g. export MODEL="./model.safetensors")
:param num_gen: number of generations to run
the training visualization through
and uses the NUM_GEN
environment variable
(e.g. export NUM_GEN=5)
:param target_faces: number of polygon faces
to extract and draw per tensor weight
and uses the FACES
environment variable
(e.g. export FACES=20000)
:param target_rows: number of rows (width) for
the 3d array in blender and the gif
and uses the ROWS
environment variable
(e.g. export ROWS=256)
:param target_cols: number of columns (height) for
the 3d array in blender and the gif
and uses the COLS
environment variable
(e.g. export COLS=256)
:param layer_names: comma-delimited list of layers
in the model weights to include in blender
and the gif
and uses the LAYER_NAMES
environment variable
(e.g. export LAYER_NAMES="h.2.,h.3.")
:param max_layers: number of layers to display
in blnder and the gif
and uses the MAX_LAYERS
environment variable
(e.g. export MAX_LAYERS=5)
:param save_gif: set to a file path and each
visualization loop will save the camera's
flying animation as a gif
and uses the GIF
environment variable
(e.g. export GIF="./blender/view-model")
:param save_gltf: optional - path to save
the blender scene as a
glTF file
:param save_stl: optional - path to save
the blender scene as an
STL file
:param num_frames: number of frames for the
animation
and uses the FRAMES
environment variable
(e.g. export FRAMES=5)
:param output_dir: output directory for temporary
animation image files (PNG or TIFF 16 bit)
and uses the OUTPUT_DIR
environment variable
(e.g. export OUTPUT_DIR="./.tmp")
:param decimation_ratio: reduce each
layer this percentage with supported values between
0.0 and 1.0
:param shutdown: flag to automatically shutdown
blender after each visualization loop
and uses the SHUTDOWN_ENABLED
environment variable
(e.g. export SHUTDOWN_ENABLED="1")
:raises SystemExit: thrown to shutdown blender
without using the mouse
"""
# https://blender.stackexchange.com/questions/5208/prevent-splash-screen-from-being-shown-when-using-a-script
bpy.context.preferences.view.show_splash = False
timeseries_training_data = []
use_layer_names = []
total_gens = "inf"
if model_file is None:
model_file = os.getenv(
"MODEL", "./model.safetensors"
)
if model_file == "":
log.error(
f"missing required model_file={model_file}"
)
if not os.path.exists(model_file):
log.error(f"failed to find model_file={model_file}")
return None
if num_gen is None:
num_gen = int(os.getenv("NUM_GEN", "1"))
if target_rows is None:
# set a target row/column for compressing very large
# tensor weights to a common resolution for viewing in 3d
# note: camera/animation untested under 50x50 for now
target_rows = int(os.getenv("ROWS", "256"))
if target_cols is None:
target_cols = int(os.getenv("COLS", "256"))
if layer_names is None:
# comma-delimited prefix (with wildcard) layer names to include
test_layer = os.getenv("LAYER_NAMES", None)
if test_layer:
layer_names = test_layer.split(",")
# by default 0/None will show all model layers
else:
use_layer_names = layer_names.split(",")
if max_layers is None:
max_layers_org = os.getenv("MAX_LAYERS", None)
if max_layers_org and len(max_layers_org) > 0:
max_layers = int(max_layers_org)
if target_faces is None:
# more faces/shapes uses more cpu + ram
target_faces = int(os.getenv("FACES", "20000"))
if num_frames is None:
# more faces/shapes uses more cpu + ram
# ~3 min to save a gif of 60 frames with 12 cpu 24GB ram
num_frames = int(os.getenv("FRAMES", "5"))
if save_gif is None:
# more faces/shapes uses more cpu + ram
# save the animation gif to this path
save_gif = os.getenv("GIF", None)
if save_stl is None:
save_stl = os.getenv("STL", None)
if save_gltf is None:
save_gltf = os.getenv("GLTF", None)
if output_dir is None:
# more faces/shapes uses more cpu + ram
# save animation temp images in this directory
output_dir = os.getenv("OUTPUT_DIR", "./.tmp")
if decimation_ratio is None:
# decimation will remove a lot of shape granularity
# but it will hopefully? work on more diverse hardware
decimation_ratio_val = os.getenv("DECIMATION", None)
if (decimation_ratio_val is not None) and (
decimation_ratio_val != ""
):
decimation_ratio = float(decimation_ratio_val)
# shutdown the blender ui if set to 1 (for automating gifs)
if shutdown is None:
if os.getenv("SHUTDOWN_ENABLED", "0") == "1":
shutdown = True
cur_idx = 0
raise_ex = False
not_done = True
while not_done:
# python 3.12 utc dates
utc_now = datetime.datetime.now(
datetime.timezone.utc
)
utc_str = utc_now.strftime("%Y-%m-%dT%H:%M:%S")
try:
# support for many versions
# over time with the same file prefix
use_gif = None
use_stl = None
use_gltf = None
# requires more hardware to do gifs
if save_gif:
if num_gen == 1:
use_gif = save_gif
else:
use_gif = f"{save_gif}.{cur_idx}.gif"
if save_stl:
if num_gen == 1:
use_stl = save_stl
else:
use_stl = f"{save_stl}.{cur_idx}.stl"
if save_gltf:
if num_gen == 1:
use_gltf = save_gltf
else:
use_gltf = f"{save_gltf}.{cur_idx}"
log.info(
"start "
f"gen={cur_idx}/{total_gens} "
f"model={model_file} "
f"faces={target_faces} "
f"target=({target_rows}, {target_cols}) "
f"layers={','.join(use_layer_names)} "
f"max_layers={max_layers} "
f"gif={use_gif} "
f"gltf={use_gltf} "
f"stl={use_stl} "
f"frames={num_frames} "
f"output_dir={output_dir} "
f"decimation={decimation_ratio} "
f"shutdown={shutdown} "
""
)
# visualize and create shapes report for training
# and export artifacts for timeseries animations
training_report = draw_layers.draw_model_layers(
input_file=model_file,
layer_names=use_layer_names,
target_rows=target_rows,
target_cols=target_cols,
target_faces=target_faces,
max_layers=max_layers,
num_frames=num_frames,
output_dir=output_dir,
save_gif=use_gif,
save_stl=use_stl,
save_gltf=use_gltf,
decimation_ratio=decimation_ratio,
shutdown_after_animation=False,
)
data_row = {
"date": utc_str,
"report": training_report,
}
timeseries_training_data.append(data_row)
if cur_idx >= num_gen:
log.info(
f"hit iteration {cur_idx}/{num_gen}"
)
if shutdown:
# shutdown blender
log.info("shutting down blender")
raise SystemExit
log.info("shutting down blender")
break
if shutdown:
# shutdown blender
log.info("shutting down blender")
raise SystemExit
# send shutdown
except SystemExit as f:
# force shut down
if raise_ex:
log.info(
"draw_model_shapes catching SystemExit - "
"shutting down"
)
not_done = False
else:
log.info(f"detected shut down ex={f}")
raise f
except Exception as e:
log.error(
f"draw_model_shapes not handling ex={e}"
)
raise e
# coming soon - visualize model in a training/learning loop
# train ai
# export to gltf/stl
not_done = False
# end of while loop
# review detected shapes through training generations
for pidx, parent_node_org in enumerate(
timeseries_training_data
):
report_node = parent_node_org["report"]
num_training_data = len(timeseries_training_data)
for idx, row in enumerate(report_node["data_3d"]):
profiled_mc_report = parent_node_org["report"][
"mc"
][idx]
layer_name = row["name"]
mc_status = profiled_mc_report["status"]
if mc_status == 1:
log.info(
"skipped training data "
f"gen={idx}/{num_training_data} "
f"layer={layer_name}"
)
continue
row_data = row["data"]
mesh_name = profiled_mc_report["name"]
mesh_idx = profiled_mc_report["idx"]
mc_report = profiled_mc_report["closest"]
mc_size_mb = mc_report["size"]
mc_level = mc_report["level"]
mc_step_size = mc_report["step_size"]
# store these in a database and use
# bampe-attention to extract/synthesize
# additional training data
mc_vertices = mc_report["vertices"]
mc_faces = mc_report["faces"]
mc_normals = mc_report["normals"]
# decoder ring - v1
# can we extract the weight's knowledge
# this is the shape meaning/knowledge
mc_data_values = mc_report["z_values"]
# exploring passing attention matrix mask
# this is how models scan for knowledge today
mc_mask = mc_report["mask"]
num_mc_verts = len(mc_vertices)
num_mc_faces = len(mc_faces)
num_mc_normals = len(mc_normals)
num_mc_data_values = len(mc_data_values)
layer_x = row["x"]
layer_y = row["y"]
layer_z = row["z"]
# layer_desc = row['desc']
log.info(
"processing training data "
f"gen={idx}/{num_training_data} "
f"layer={layer_name} "
f"size={row_data.shape} "
f"pos=({layer_x}, "
f"{layer_y}, "
f"{layer_z}) "
""
f"mesh {mesh_idx}={mesh_name} "
f"size={mc_size_mb} "
f"cubes level={mc_level} "
f"step_size={mc_step_size} "
""
f"shapes vertices={num_mc_verts} "
f"faces={num_mc_faces} "
f"normals={num_mc_normals} "
f"values={num_mc_data_values} "
f"mask={mc_mask} "
""
)
# end of reviewing marching cubes
# end of building the training report
log.info("done")
return timeseries_training_data
Draw Model Weight Layers in a 3D Marching Cubes Mesh
Load a model's weights into blender
draw_model_layers(input_file, layer_names=[], max_layers=30, device='cpu', target_faces=None, target_rows=256, target_cols=256, x=0, y=0, z=1, rotate_z=None, max_depth=2, pad_per=20, save_gif=None, save_gltf=None, save_stl=None, output_dir=None, num_frames=10, decimation_ratio=None, shutdown_after_animation=False, auto_convert=True, file_format='PNG', color_depth='8', color_mode='RGBA', center_camera=True, animation_y_move_speed=-200)
draw_model_layers
render a model.safetensors file using blender by rescaling the model weights into polygons/shapes/faces and then drawing them in a resolution that hopefully can see the shapes (color is not supported for weights over 512x512 in shape dimensions)
Parameters: |
|
---|
Source code in bw/bl/draw_model_layers.py
def draw_model_layers(
input_file: str,
layer_names: list = [],
max_layers: int = 30,
device: str = "cpu",
target_faces: int = None,
target_rows: int = 256,
target_cols: int = 256,
x: int = 0,
y: int = 0,
z: int = 1,
rotate_z: float = None,
max_depth: int = 2,
pad_per: int = 20,
save_gif: str = None,
save_gltf: str = None,
save_stl: str = None,
output_dir: str = None,
num_frames: int = 10,
decimation_ratio: float = None,
shutdown_after_animation: bool = False,
auto_convert: bool = True,
file_format: str = "PNG",
color_depth: str = "8",
color_mode: str = "RGBA",
center_camera: bool = True,
animation_y_move_speed: int = -200,
):
"""
draw_model_layers
render a model.safetensors file using
blender by rescaling the model weights
into polygons/shapes/faces and then
drawing them in a resolution that
hopefully can see the shapes (color
is not supported for weights over
512x512 in shape dimensions)
:param input_file: path to model.safetensors
file
:param layer_names: filter by layer
layer colomn names
:param max_layers: limit the number of
layers to render
:param device: cpu vs gpu
:param target_faces: min faces to
hopefully render if there is
enough data
:param target_rows: number of
resample target rows before
drawing
:param target_cols: number of
resample target cols before
drawing
:param x: starting x axis location
for all shapes
:param y: starting y axis location
for all shapes
:param z: starting z axis location
for all shapes
:param rotate_z: starting z euler rotation
in radians
:param max_depth: stack each layer
on itself this many times
to convert it to a 3d ndarray
:param pad_per: number to pad
per object
:param save_gif: optional - path to save
the fly-through camera animation
as a single gif
:param save_gltf: optional - path to save
the blender scene as a
glTF file
:param save_stl: optional - path to save
the blender scene as an
STL file
:param output_dir: save outputs
to this dir
:param num_frames: number of animation
key frames to show before saving a gif
:param decimation_ratio: reduce each
layer this percentage with supported values between
0.0 and 1.0
:param shutdown_after_animation: flag to
shutdown the blender ui application
after saving the animation
:param auto_convert: flag to run ImageMagick
to convert all temp animation images to
a gif if set to True
:param file_format: type of file for temp
animation images ('PNG' vs 'TIFF' vs 'JPEG')
:param color_depth: '8' bit vs '16' bit
:param color_mode: 'RGBA' vs 'RBG'
:param center_camera: flag to center
the camera for animations based off
the target dimensions
:param animation_y_move_speed: how fast
does the camera fly during the animation
"""
obj_x = None
obj_y = None
obj_z = None
mesh_cube_report = []
log.info(
f"start - model={input_file} "
f"target.shape=({target_rows}, {target_cols}) "
f"faces={target_faces} "
f"max_layers={max_layers} "
f"layers={len(layer_names)} "
f"start pos=({x}, {y}, {z}) "
f"rotate_z=radians({rotate_z}) "
f"num_frames={num_frames} "
f"output_dir={output_dir} "
f"gif={save_gif} "
""
)
# extract the data using safetensors rust mmap
all_data_3d = (
extract_weights.extract_3d_shapes_from_model_file(
input_file=input_file,
layer_names=layer_names,
max_layers=max_layers,
device=device,
target_faces=target_faces,
target_rows=target_rows,
target_cols=target_cols,
start_x=x,
start_y=y,
start_z=z,
max_depth=max_depth,
pad_per=pad_per,
)
)
# Clear existing mesh objects
bwclear.clear_all_objects()
x_max_text_len = 0
num_datas = len(all_data_3d)
log.info(f"rendering {num_datas} object shapes")
for idx, data_3d in enumerate(all_data_3d):
name = data_3d["name"]
desc = data_3d["desc"]
x_max_text_len = len(desc)
data_to_render = data_3d["data"]
target_faces = data_3d["target_faces"]
obj_x = data_3d["x"]
obj_y = data_3d["y"]
obj_z = data_3d["z"]
# shutdown blender for debugging issues
# raise SystemExit
log.info(
f"rendering {idx + 1}/{num_datas} "
f"{name} "
f"pos=({obj_x}, {obj_y}, {obj_z})"
)
(
mesh_name,
mc_report,
) = mesh_gen.generate_3d_from_3d(
name=f"Layer {idx + 1}: {name}",
desc=desc,
data=data_to_render,
x=obj_x,
y=obj_y,
z=obj_z,
target_faces=target_faces,
mesh_idx=idx,
decimation_ratio=decimation_ratio,
)
# active status
status = 0
if mc_report is None:
log.info(
f"ignoring {idx + 1}/{num_datas} " f"{name}"
)
# inactive status
status = 1
else:
log.info(
f"adding {idx + 1}/{num_datas} "
f"{name} "
f"pos=({obj_x}, {obj_y}, {obj_z}) "
)
mesh_cube_report.append(
{
"name": mesh_name,
"status": status,
"idx": idx,
"layer_name": name,
"target_faces": target_faces,
"closest": mc_report,
"data_3d": data_3d,
}
)
# end of drawing 3d objects
# calculate the center for the rectangle
width_total = target_cols + 50
width_center = x + int(width_total / 2)
height_per_layer = pad_per + max_depth + 1
height_total = len(mesh_cube_report) * height_per_layer
height_total = (len(mesh_cube_report) * pad_per) + 5
height_center = y + int(height_total / 2)
if target_cols < 256:
width_total = 280
width_center = x + int(width_total / 2) - 5
log.debug(
"centering rectangle "
f"width_total={width_total} "
f"width_center={width_center} "
f"max_desc_len={x_max_text_len} "
f"height_total={height_total} "
f"height_center={height_center} "
f"height_per_layer={height_per_layer} "
f"src pos=({x}, {y}, {z}) "
f"dst pos=({x + width_center}, "
f"{y + height_center - 10}, "
f"{z})"
)
# Create background under the layers
bwcr.create_rectangle(
height=height_total,
width=width_total,
depth=0.1,
hex_color="#000000", # black
opacity=1.0,
x_position=x + width_center,
y_position=y + height_center - 15,
z_position=z,
)
# save the data to various locations
if save_stl:
export_stl.save_as_stl(output_path=save_stl)
if not os.path.exists(save_stl):
log.error(f"failed to save stl={save_stl}")
if save_gltf:
export_gltf.save_as_gltf(
output_path=save_gltf, export_format="GLB"
)
if not os.path.exists(save_stl):
log.error(f"failed to save gltf={save_gltf}")
if save_gif:
bwan.save_animation(
save_gif=save_gif,
output_dir=output_dir,
frame_start=1,
frame_end=num_frames,
center_camera=center_camera,
target_rows=target_rows,
target_cols=target_cols,
x_start=obj_x,
y_start=y,
z_start=obj_z,
x_end=obj_x,
y_end=y + 500,
z_end=obj_z,
x_move_speed=0,
y_move_speed=animation_y_move_speed,
z_move_speed=0,
shutdown_after_animation=shutdown_after_animation,
auto_convert=auto_convert,
file_format=file_format,
color_depth=color_depth,
color_mode=color_mode,
)
return {
"data_3d": all_data_3d,
"mc": mesh_cube_report,
"stl": save_stl,
"gltf": save_gltf,
"gif": save_gif,
}
Draw 3D mesh from 3D array
Create a 3d marching cubes representation as a mesh from a 3d array
generate_3d_from_3d(data, target_faces=None, mask=None, name=None, name_color='#FFFFFF', name_opacity=1.0, name_extrude=1.7, desc=None, desc_color='#555555', desc_opacity=1.0, desc_extrude=1.5, x=0.0, y=0.0, z=0.0, vertex_scale_size=1.0, color_percentile=95.0, background_enabled=False, background_color='#000000', background_opacity=1.0, background_height=None, background_width=None, background_depth=None, background_x=None, background_y=None, background_z=None, level=1.0, step_size=1, clean_workspace=False, target_mb=None, mc_report_file=None, mesh_idx=None, num_colors=5, decimation_ratio=None, safe_for_colors_in_ram=False)
generate_3d_from_3d
generate a marching cubes representation of the underlying 3d data based off statistical relevance in the float32 data.
supports targeting colors in the mesh based off a target percentile in the z-axis value of the source 3d array
returns tuple ( mesh_name, closest_report, )
Parameters: |
|
---|
Source code in bw/bl/generate_3d_from_3d.py
def generate_3d_from_3d(
data: np.ndarray,
target_faces: int = None,
mask: np.ndarray = None,
name: str = None,
name_color: str = "#FFFFFF",
name_opacity: float = 1.0,
name_extrude: float = 1.7,
desc: str = None,
desc_color: str = "#555555",
desc_opacity: float = 1.0,
desc_extrude: float = 1.5,
x: float = 0.0,
y: float = 0.0,
z: float = 0.0,
vertex_scale_size: float = 1.0,
color_percentile: float = 95.0,
background_enabled: bool = False,
background_color: str = "#000000",
background_opacity: float = 1.0,
background_height: int = None,
background_width: int = None,
background_depth: int = None,
background_x: float = None,
background_y: float = None,
background_z: float = None,
level: float = 1.0,
step_size: int = 1,
clean_workspace: bool = False,
target_mb: float = None,
mc_report_file: str = None,
mesh_idx: int = None,
num_colors: int = 5,
decimation_ratio: float = None,
safe_for_colors_in_ram: bool = False,
):
"""
generate_3d_from_3d
generate a marching cubes representation
of the underlying 3d data based off statistical
relevance in the float32 data.
supports targeting colors in the mesh based
off a target percentile in the z-axis value of
the source 3d array
returns tuple (
mesh_name,
closest_report,
)
:param data: 3d array data
:param target_faces: optional - find the nearest
marching cubes configuration by resulting
number of faces in the volume
:param mask: 3d array for mask
:param name: optional - name for labeling the
mesh to the left
:param name_color: hex color string for the name text
:param name_opacity: opacity for the name text
:param name_extrude: extusion amount for the name text
:param desc: optional - description for labeling the
mesh to the left
:param desc_color: hex color string for the desc text
:param desc_opacity: opacity for the desc text
:param desc_extrude: extusion amount for the name text
:param x: x position for the mesh
:param y: y position for the mesh
:param z: z position for the mesh
:param level: level for the marching
cubes algorithm
:param vertex_scale_size: apply a scaler to each vertex
:param color_percentile: target percentile
for applying a color to a cube face
:param background_enabled: flag for including
drawing a rectangle under the mesh
(and optional text)
:param background_color: hex color string
with default #000000 for black
:param background_opacity: transparency for
the background
with default no transparency at 1.0
:param background_height: how tall is the background
:param background_width: how wide is the background
:param background_depth: how deep is the background
:param background_x: x position for the background
:param background_y: y position for the background
:param background_z: z position for the background
:param step_size: number of marching cube step sizes
:param clean_workspace: flag for deleting all
objects before rendering
:param target_mb: optional - find the nearest
marching cubes configuration by resulting
megabyte size (more useful to use
target_faces)
:param mc_report_file: optional - path to save
the mc_report slim dictionary as a json
file
:param num_colors: optional - number of
different colors (too many colors will
cause the mesh to fail drawing)
:param decimation_ratio: reduce each
layer this percentage with supported values between
0.0 and 1.0
:param safe_for_colors_in_ram: flag to
force-enable colors. coloring this much
data is very expensive so it is off
by default
"""
# for debugging
if clean_workspace:
bwclear.clear_all_objects()
if not mesh_idx:
mesh_idx = 1
mesh_name = f"Mesh_{mesh_idx}"
mesh_obj_name = f"MeshObj_{mesh_idx}"
# Create a new mesh
mesh = bpy.data.meshes.new(name=mesh_name)
mesh_obj = bpy.data.objects.new(mesh_obj_name, mesh)
# Link the mesh to the scene
bpy.context.scene.collection.objects.link(mesh_obj)
bpy.context.view_layer.objects.active = mesh_obj
mesh_obj.select_set(True)
# Flip the array to have the desired orientation (x, z, y)
data = np.transpose(data, (0, 2, 1))
# Apply marching cubes algorithm and build a report
mc_report = bwmc.profile_data_with_cubes(
data=data,
data_name=name,
target_mb=target_mb,
target_faces=target_faces,
save_to_file=mc_report_file,
include_vertices=True,
include_normals=True,
include_faces=True,
include_masks=True,
)
if mc_report is None:
mc_report = bwmc.profile_data_with_cubes(
data=data,
data_name=name,
target_mb=target_mb,
target_faces=1000,
save_to_file=mc_report_file,
include_vertices=True,
include_normals=True,
include_faces=True,
include_masks=True,
)
if mc_report is None:
log.debug(
"failed to find a marching cube profile for "
f"name={name}"
)
return (
mesh_name,
None,
)
# Find the closest report based off the target
closest_report = mc_report["closest"]
if len(closest_report) == 0:
log.error(
"failed to find a marching cube configuration "
f"name={name} "
f"that targets faces={target_faces} "
f"size_mb={target_mb} "
"stopping"
)
# raise SystemExit
return (
mesh_name,
None,
)
vertices = closest_report["vertices"]
faces = closest_report["faces"]
# normals = closest_report['normals']
mc_z_values = closest_report["z_values"]
closest_level = closest_report["level"]
closest_step_size = closest_report["step_size"]
closest_desc = closest_report["desc"]
num_vertices = len(vertices)
num_faces = len(faces)
log.debug(
f"mc {closest_desc} target_faces={target_faces} "
f"level={closest_level} "
f"step_size{closest_step_size} "
f"from src data.shape={data.shape} "
f"cubes z_values.shape={mc_z_values.shape} "
f"vertices={num_vertices} "
f"faces={num_faces} "
f"level={level} step_size={step_size} "
f"decimation={decimation_ratio} "
"calculated "
""
)
z_values = mc_z_values
mesh.from_pydata(vertices, [], faces)
mesh.update()
# reduce the mesh marching cube complexity
if decimation_ratio:
if 0.0 < decimation_ratio < 1.0:
if False:
decimator.apply_decimator(
name=mesh_obj_name,
decimation_ratio=decimation_ratio,
)
else:
log.error(
f"invalid decimation_ratio={decimation_ratio} "
"only values between 0.0 and 1.0 "
"are supported"
)
# Create a BMesh
bm = bmesh.new()
# Create vertices
log.debug(
"rendering mesh "
f"vertices={len(vertices)} "
f"faces={len(faces)}"
)
# scale the vertices
for v in vertices:
bm.verts.new(
(
v[0] * vertex_scale_size,
v[1] * vertex_scale_size,
v[2] * vertex_scale_size,
)
)
# Ensure lookup table for vertices
bm.verts.ensure_lookup_table()
# Create faces
for f in faces[:-3]:
bm.faces.new([bm.verts[i] for i in f])
# Ensure lookup table for faces
bm.faces.ensure_lookup_table()
# determine colors based off the min/max values
# in the z-axis
# use the face's z weights to
min_values = []
max_values = []
z_values = []
z_min = None
z_max = None
num_faces = len(faces)
if num_faces > 5000:
if safe_for_colors_in_ram:
log.critical(
"warning consider disabling colors to save on ram "
f"for num_faces={num_faces}"
)
if safe_for_colors_in_ram:
log.debug(f"colorizing num_faces={num_faces}")
for face in bm.faces:
z_values_face = np.array(
[v.co.z for v in face.verts]
)
z_min = np.min(z_values_face)
z_max = np.max(z_values_face)
break
for face in bm.faces:
z_values_face = np.array(
[v.co.z for v in face.verts]
)
if z_values_face.size > 0:
cur_min = np.min(z_values_face)
cur_max = np.max(z_values_face)
if z_min > cur_min:
z_min = cur_min
# add the min to the front
min_values.insert(0, cur_min)
else:
min_values.append(cur_min)
if z_max < cur_max:
z_max = cur_max
# add the max to the end
max_values.append(cur_max)
else:
max_values.insert(0, cur_max)
# partially-sorted z_values
z_values = np.array(min_values + max_values)
if z_min:
z_min -= 1.0
if z_max:
z_max += 1.0
log.debug(
f"using face z_values: {z_values} "
f"[{z_min},{z_max}]"
)
# get build the colors based off the max/min
color_dict = bwqc.get_quantile_colors(
min_value=z_min,
max_value=z_max,
num=num_colors,
opacity=0.5,
z_values=z_values,
)
# total_weight = np.sum(z_values)
# Colorize faces based on weighted percentile
for face in bm.faces:
z_values_face = np.array(
[v.co.z for v in face.verts]
)
# Change to desired percentile
weighted_percentile = np.percentile(
z_values_face, color_percentile
)
color = assign_color.assign_color(
weighted_percentile, color_dict
)
# if you hit this, you cannot draw anymore faces/colors
# try/ex will just run out of ram
face.material_index = (
assign_material.assign_material(color, mesh)
)
# if able to support coloring with ram
# Set the location of the object
mesh_x = x
mesh_y = y
mesh_z = z
if name:
add_text.add_text(
text=name,
position=(x, y, z),
color=name_color,
opacity=name_opacity,
extrude=name_extrude,
)
mesh_z += 5
mesh_x += 20
if desc:
desc_x = x
desc_y = y
desc_z = z
if name:
desc_x = x
desc_y = y - 10
desc_z = z
add_text.add_text(
text=desc,
position=(desc_x, desc_y, desc_z),
color=desc_color,
opacity=desc_opacity,
extrude=desc_extrude,
)
mesh_obj.location = (mesh_x, mesh_y, mesh_z)
# Update the BMesh and free it
bm.to_mesh(mesh)
bm.free()
# Create the background if enabled
if background_enabled:
if not background_x:
background_x = x - 2
if not background_y:
background_y = y - 2
if not background_z:
background_z = z
if not background_height:
background_height = mesh_y + 200
if not background_width:
background_width = 200
if not background_depth:
background_depth = 2
log.debug(
f"drawing background({background_x}, "
f"{background_y}, {background_z}) "
f"dim=({background_height}, "
f"{background_width}, "
f"{background_depth})"
)
cr.create_rectangle(
height=background_height,
width=background_width,
depth=background_depth,
hex_color=background_color,
opacity=background_opacity,
x_position=background_x,
y_position=background_y,
z_position=background_z,
)
return (
mesh_name,
closest_report,
)
Blender 3D Object APIs
Here are the supported 3d apis.
Assign Colors based off Weighted Value vs rest of the 3D array
assign_color(percentile, color_dictionary)
assign_color
assign a color based off the percentile value based off the lower/upper min/max values in the color_dictionary
returns the color_tuple for blender in decimal format (1.0, 1.0, 1.0, 1.0) where (R, G, B, A)
Parameters: |
|
---|
Source code in bw/bl/assign_color.py
def assign_color(
percentile: float,
color_dictionary: dict,
):
"""
assign_color
assign a color based off the percentile
value based off the lower/upper min/max values
in the color_dictionary
returns the color_tuple for blender in decimal
format (1.0, 1.0, 1.0, 1.0) where (R, G, B, A)
:param percentile: percentile that needs
a color
:param color_dictionary: source of truth for
how to colorize percentiles based off
pre-existing setup
(for more refer to bw.bl.get_percentile_colors)
"""
min_value = None
max_value = None
color = None
for range_key, perc_node in color_dictionary.items():
min_value = perc_node["min"]
max_value = perc_node["max"]
"""
log.debug(
f"{min_value} <= {percentile} "
f"<= {max_value}"
)
"""
color = (
perc_node["r"],
perc_node["b"],
perc_node["g"],
perc_node["a"],
)
if min_value <= percentile <= max_value:
"""
name = perc_node["name"]
log.debug(
f"{min_value} <= {percentile} "
f"<= {max_value} => "
f"{name} = {color}"
)
"""
return color
"""
log.error(
"unsupported percentile detected "
f"{min_value} <= {percentile} "
f"<= {max_value} => "
f"{color}"
)
"""
return color
Assign Material
assign_material(color, mesh)
assign_material
assign a color tuple to a material in blender
returns int for the new number of materials in the mesh
Source code in bw/bl/assign_material.py
def assign_material(
color,
mesh,
):
"""
assign_material
assign a color tuple to a material in blender
returns int for the new number of materials in the mesh
"""
# Create a new material with the specified color
mat = bpy.data.materials.new(
name=f"Material_{len(bpy.data.materials)}"
)
mat.use_nodes = (
False # Disable node-based materials for simplicity
)
mat.diffuse_color = color
mesh.materials.append(mat)
# Return the material index
return len(mesh.materials) - 1
Add Colorized 3D Rectangle, Cube, or Wall in Blender
Add a 3d rectangle, cube or wall.
create_rectangle(height, width, depth, hex_color, opacity, x_position, y_position, z_position)
create_rectangle
create a 3d rectangle by x, y, z with rgba support
Parameters: |
|
---|
Source code in bw/bl/create_rectangle.py
def create_rectangle(
height: int,
width: int,
depth: int,
hex_color: str,
opacity: float,
x_position: int,
y_position: int,
z_position: int,
):
"""
create_rectangle
create a 3d rectangle by x, y, z with
rgba support
:param height: height of the object
:param width: width of the object
:param depth: depth of the object
:param hex_color: hex color string
:param opacity: transparency
:param x_position: x location
:param y_position: y location
:param z_position: z location
"""
color = tuple(
int(hex_color[i : i + 2], 16) / 255.0
for i in (1, 3, 5)
)
log.debug(
"creating rectangle("
f"x={x_position}, "
f"y={y_position}, "
f"z={z_position}, "
f"dep={depth}"
)
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(x_position, y_position, z_position),
)
rectangle = bpy.context.active_object
rectangle.dimensions = (width, height, depth)
material = bpy.data.materials.new(
name="Rectangle_Material"
)
rectangle.data.materials.append(material)
material.use_nodes = False
material.diffuse_color = color + (opacity,)
log.debug("done")
Add Colorized 3D Text in Blender
Convert a text string to a 3d text message in blender at an (x, y, z) position with coloring, font style, size and extrusion depth
add_text(text, position=(0, 0, 0), font_size=8, font_style=None, font_family_path=None, color='#FFFFFF', opacity=1.0, extrude=0.1, bevel_depth=0.05)
add_text
add a text string at x, y, z with opacity
Parameters: |
|
---|
Source code in bw/bl/add_text.py
def add_text(
text: str,
position: tuple = (0, 0, 0),
font_size: int = 8,
font_style: str = None,
font_family_path: str = None,
color: str = "#FFFFFF",
opacity: float = 1.0,
extrude: float = 0.1,
bevel_depth: float = 0.05,
):
"""
add_text
add a text string at x, y, z with opacity
:param text: message to show in blender
:param position: (x, y, z) integer location
for the text
:param font_size: size of the font
:param font_style: style for the font
like BOLD
:param font_family_path: optional path to the
font family file to use
:param color: hex color string for the text
:param opacity: alpha color for the text
:param extrude: amount to extend the text
:param bevel_depth: amount to curve the text
edges
"""
bpy.ops.object.text_add(
enter_editmode=False,
align="WORLD",
location=position,
)
text_obj = bpy.context.active_object
text_obj.data.body = text
text_obj.data.size = font_size
if font_family_path and os.path.exists(
font_family_path
):
text_obj.data.font = bpy.data.fonts.load(
font_family_path
)
if font_style:
# font_sytyle = 'BOLD'
text_obj.data.style = font_style
rgb_tuple = hex_to_rgb(color)
rgba_color = (
rgb_tuple[0],
rgb_tuple[1],
rgb_tuple[2],
opacity,
)
"""
# for debugging
print(rgba_color)
"""
text_obj.color = rgba_color
text_obj.data.extrude = (
extrude # Adjust extrude value as needed
)
bpy.ops.object.convert(target="MESH")
# Select the newly created mesh object
bpy.context.view_layer.objects.active = (
bpy.context.scene.objects[text_obj.data.name]
)
# Set material properties
mat = bpy.data.materials.new(name="TextMaterial")
mat.use_nodes = False
mat.diffuse_color = rgba_color
text_obj.data.materials.append(mat)
hex_to_rgb(hex_color)
hex_to_rgb
convert hex to floats returned in a tuple (r,b,g)
Parameters: |
|
---|
Source code in bw/bl/add_text.py
def hex_to_rgb(hex_color: str):
"""
hex_to_rgb
convert hex to floats returned in a tuple (r,b,g)
:param hex_color: hexidecimal color string
"""
return tuple(
int(hex_color[i : i + 2], 16) / 255.0
for i in (1, 3, 5)
)
Clear all Objects in Blender
Remove all objects in the blender workspace
clear_all_objects()
clear_all_objects
clear all objects in the blender env
Source code in bw/bl/clear_all_objects.py
def clear_all_objects():
"""
clear_all_objects
clear all objects in the blender env
"""
log.debug("start")
# clear all mesh objects from the scene
bpy.ops.object.select_all(action="DESELECT")
# iterate through all objects in the scene
for obj in bpy.context.scene.objects:
# optional filtering by names
# if obj.type == 'MESH' or (obj.type == 'MESH' and obj.name == 'Cube'):
if obj.type == "MESH":
obj.select_set(True)
# delete selected objects
bpy.ops.object.delete()
Decimators
Reduce the Active Object with a Blender Decimator
Note: decimators are great because they reduce the host requirements to run the visualizer, but they also remove a ton of shape details. Use at your own risk. This is why the skimage.measure.marching_cubes algorithm is used for preprocess shape (vertices, faces, normals, values) detection before decimation and coloring.
Using a decimator in Blender is beneficial for rendering performance because it reduces the polygon count of a 3D model. This optimization helps decrease the computational load during rendering, making the process faster and more efficient. The decimator simplifies complex geometry, maintaining the overall shape while reducing the number of vertices, edges, and faces. This is especially useful when working with intricate models or scenes to achieve a balance between visual quality and rendering speed.
apply_decimator(name, decimation_ratio=0.5)
apply_decimator
a decimator in blender is beneficial for rendering performance because it reduces the polygon count of a 3D model. this optimization helps decrease the computational load during rendering, making the process faster and more efficient. The decimator simplifies complex geometry, maintaining the overall shape while reducing the number of vertices, edges, and faces. This is especially useful when working with intricate models or scenes to achieve a balance between visual quality and rendering speed.
Parameters: |
|
---|
Source code in bw/bl/decimator_on_object.py
def apply_decimator(
name: str, decimation_ratio: float = 0.5
):
"""
apply_decimator
a decimator in blender is beneficial for
rendering performance because it reduces
the polygon count of a 3D model. this
optimization helps decrease the
computational load during rendering,
making the process faster and more
efficient. The decimator simplifies complex
geometry, maintaining the overall shape
while reducing the number of vertices,
edges, and faces. This is especially
useful when working with intricate
models or scenes to achieve a balance
between visual quality and rendering speed.
:param name: name of the object
to decimate
:param decimation_ratio: percent to
reduce the object between 0.0 and 1.0
"""
found_it = bpy.data.objects.get(name)
if found_it is None:
log.error(f"unable to find bpy object_name={name}")
return False
log.debug(
"start "
f"decimate=(name={name}, ratio={decimation_ratio})"
)
# Select the specified object
bpy.context.view_layer.objects.active = (
bpy.data.objects.get(name)
)
bpy.context.active_object.select_set(True)
# Switch to Object mode
bpy.ops.object.mode_set(mode="OBJECT")
# Add a Decimate modifier
bpy.ops.object.modifier_add(type="DECIMATE")
decimate_modifier = bpy.context.object.modifiers[
"Decimate"
]
# Set the decimation ratio
decimate_modifier.ratio = decimation_ratio
# Apply the modifier to permanently modify the mesh
bpy.ops.object.modifier_apply(
{"object": bpy.context.active_object},
modifier="Decimate",
)
log.debug(
"done "
f"decimate=(name={name}, "
f"ratio={decimation_ratio})"
)
return True
apply_decimator_to_object(active_object, reduce_percentage=0.5)
apply_decimator_to_object
a decimator in blender is beneficial for rendering performance because it reduces the polygon count of a 3D model. this optimization helps decrease the computational load during rendering, making the process faster and more efficient. The decimator simplifies complex geometry, maintaining the overall shape while reducing the number of vertices, edges, and faces. This is especially useful when working with intricate models or scenes to achieve a balance between visual quality and rendering speed.
Parameters: |
|
---|
Source code in bw/bl/decimator_on_active_object.py
def apply_decimator_to_object(
active_object,
reduce_percentage: float = 0.5,
):
"""
apply_decimator_to_object
a decimator in blender is beneficial for
rendering performance because it reduces
the polygon count of a 3D model. this
optimization helps decrease the
computational load during rendering,
making the process faster and more
efficient. The decimator simplifies complex
geometry, maintaining the overall shape
while reducing the number of vertices,
edges, and faces. This is especially
useful when working with intricate
models or scenes to achieve a balance
between visual quality and rendering speed.
:param active_object: set this
to the blender active object to decimate (reduce)
active_object=bpy.context.active_object
:param reduce_percentage: set the target
reduction percentage as a value between 0.0 and 1.0
"""
log.info("start reduce={reduce_percentage}")
modifier = active_object.modifiers.new(
name="Decimate", type="DECIMATE"
)
# Adjust the ratio based on your decimation requirements
modifier.ratio = reduce_percentage
# Apply the modifier to permanently modify the mesh
bpy.ops.object.modifier_apply(
{"object": active_object}, modifier="Decimate"
)
log.info("done reduce={reduce_percentage}")
Blender 3D Camera and Animation APIs
Render Camera Animation
Render the camera animation key frames.
render_animation(save_gif=False, gif_path=None)
render_animation
render the animation frames and save as a gif if set
Parameters: |
|
---|
Source code in bw/bl/render_animation.py
def render_animation(
save_gif: bool = False, gif_path: str = None
):
"""
render_animation
render the animation frames and save as
a gif if set
:param save_gif: flag to enable saving
the animation as a gif
:param gif_path: path to save the gif
"""
if save_gif:
log.debug("saving gif={save_gif}")
bpy.context.scene.render.filepath = gif_path
bpy.ops.render.render(animation=True)
bpy.ops.image.save_as(
{
"active_object": bpy.data.images[
"Render Result"
]
},
copy=True,
filepath=gif_path,
check_existing=False,
)
else:
log.debug("not saving gif={save_gif}")
bpy.ops.render.render(animation=True)
Save Camera Animation as a GIF
Note: saving a gif animation over many FRAMES is resource intensive. Please monitor utilization.
save_animation(save_gif, output_dir=None, frame_start=1, frame_end=2, center_camera=True, target_rows=None, target_cols=None, x_start=-300, y_start=0, z_start=200, x_end=-300, y_end=500, z_end=200, x_move_speed=0, y_move_speed=-10, z_move_speed=0, shutdown_after_animation=True, auto_convert=True, file_format='PNG', color_depth='8', color_mode='RGBA')
save_animation
save the key frame animation to a gif after setting up the camera and temporary file storage
Parameters: |
|
---|
Raises: |
|
---|
Source code in bw/bl/save_animation.py
def save_animation(
save_gif: str,
output_dir: str = None,
frame_start: int = 1,
frame_end: int = 2,
center_camera: bool = True,
target_rows: int = None,
target_cols: int = None,
x_start: int = -300,
y_start: int = 0,
z_start: int = 200,
x_end: int = -300,
y_end: int = 500,
z_end: int = 200,
x_move_speed: int = 0,
y_move_speed: int = -10,
z_move_speed: int = 0,
shutdown_after_animation: bool = True,
auto_convert: bool = True,
file_format: str = "PNG",
color_depth: str = "8",
color_mode: str = "RGBA",
):
"""
save_animation
save the key frame animation to a gif
after setting up the camera and temporary
file storage
:param save_gif: path to save the animation
as a gif file
:param center_camera: flag to center
the camera for animations based off
the target dimensions
:param target_rows: number of rows for
camera centering
:param target_cols: number of columns for
camera centering
:param output_dir: temp directory
for saving animation images before
gif compilation
:param frame_start: start frame idx
usually 1
:param frame_end: ending frame idx
:param x_start: camera start x position
:param y_start: camera start y position
:param z_start: camera start z position
:param x_end: camera end z position
:param y_end: camera end z position
:param z_end: camera end z position
:param x_move_speed: speed the camera
navigates from the
x_start to the x_end position
:param y_move_speed: speed the camera
navigates from the
y_start to the y_end position
:param z_move_speed: speed the camera
navigates from the
z_start to the z_end position
:param shutdown_after_animation: flag to
shutdown after rendering the animation
to a gif. helpful to set to
False if there are no shapes
in the gif. in blender you can see the
camera's location and route for debugging.
:param auto_convert: flag to run ImageMagick
to convert all temp animation images to
a gif if set to True
:param file_format: type of file for temp
animation images ('PNG' vs 'TIFF' vs 'JPEG')
:param color_depth: '8' bit vs '16' bit
:param color_mode: 'RGBA' vs 'RBG'
:raises SystemExit: if the shutdown_after_animation
flag is set to True, this will exit blender after
saving the gif or if the output directory
failed creation. if blender exits, then it will
be difficult to debug camera
location/pathing issues.
"""
if center_camera:
cam_start_x = x_start
cam_start_y = y_start
cam_start_z = z_start
cam_end_x = x_end
cam_end_y = y_end
cam_end_z = z_end
if target_rows and target_cols:
# works for 50x50
# rows on the x-axis
center_x = int(target_rows) - 5
# cols on the z-axis
center_z = int(target_cols) - 10
# larger than 128x128 divide in half
if target_rows > 128:
center_x = int(int(target_rows) / 2)
if target_cols > 128:
center_z = int(int(target_cols) / 2)
log.info(
"animation camera centering "
""
f"mesh_pos=({x_start}, "
f"{y_start}, "
f"{z_start}) "
""
"cam_pos=("
f"{cam_start_x + center_x}, "
f"{cam_start_y}, "
f"{cam_start_z + center_z}) "
f"x={cam_start_x} "
f"x_center={center_x} "
f"z={cam_start_z} "
f"z_center={center_z}"
)
cam_start_x += center_x
cam_end_x += center_x
cam_start_z += center_z
cam_end_z += center_z
else:
log.info("using cam defaults")
x_start = cam_start_x
y_start = cam_start_y
z_start = cam_start_z
x_end = cam_end_x
y_end = cam_end_y
z_end = cam_end_z
# end center_camera
log.info(
f"saving frames=[{frame_start}, {frame_end}] "
f"gif={save_gif} center={center_camera} "
f"start camera=({x_start}, {y_start}, {z_start}) "
f"end camera=({x_end}, {y_end}, {z_end}) "
f"speed=(x={x_move_speed}, y={y_move_speed}, "
f"z={z_move_speed}) "
f"shutdown={shutdown_after_animation} "
f"output_dir={output_dir} "
""
)
if not save_gif:
log.info(f"no gif to save={save_gif}")
return
# Set the output directory and file name
if not output_dir:
output_dir = os.getenv("GIF_DIR", "./.tmp")
if not os.path.exists(output_dir):
os.mkdir(output_dir)
if not os.path.exists(output_dir):
log.error(
f"unable to create output dir: {output_dir} - stopping"
)
raise SystemExit
output_file = save_gif
output_path = output_file
camera_name = "Camera"
tmp_output_path = f"{output_dir}/temp_frame_"
set_render_settings.set_render_settings(
file_format=file_format,
color_mode=color_mode,
color_depth=color_depth,
use_zbuffer=False,
color_alpha=False,
)
set_background_color.set_background_color(
use_alpha=False,
world_r=1.0,
world_g=1.0,
world_b=1.0,
)
set_output_file.set_output_file(
filepath=tmp_output_path,
file_format=file_format,
)
set_cam.set_camera_location_orientation(
name=camera_name,
x_start=x_start,
y_start=y_start,
z_start=z_start,
x_end=x_end,
y_end=y_end,
z_end=z_end,
)
set_animation.set_animation_parameters(
frame_start=frame_start,
frame_end=frame_end,
x_start=x_start,
y_start=y_start,
z_start=z_start,
x_move_speed=x_move_speed,
y_move_speed=y_move_speed,
z_move_speed=z_move_speed,
)
# determine num frames after detection
sc_frame_start = bpy.context.scene.frame_start
sc_frame_end = bpy.context.scene.frame_end
num_frames = sc_frame_end - sc_frame_start
log.info(f"rendering animation for {num_frames} frames")
# automated render animation
bpy.ops.render.render(animation=True)
# convert frames to gif using an external tool like ImageMagick
if auto_convert:
vc = (
f"convert -delay 2 -loop 0 "
f"{output_dir}/temp_frame_*.png "
f"{output_path}"
)
if file_format == "TIFF":
vc = (
f"convert -delay 2 -loop 0 "
f"{output_dir}/temp_frame_*.tif "
f"{output_path}"
)
elif file_format == "JPEG":
vc = (
f"convert -delay 2 -loop 0 "
f"{output_dir}/temp_frame_*.jpeg "
f"{output_path}"
)
log.info(
f"saving frames={num_frames} animation with command: {vc}"
)
os.system(vc)
else:
log.info("not converting gif")
# clean up temporary frames
for item in bpy.data.images:
log.info(f"cleaning up animation images: {item}")
if item.name.startswith("temp_frame_"):
bpy.data.images.remove(item)
if shutdown_after_animation:
log.info("animation done - shutting down")
raise SystemExit
log.info(f"done saving gif={save_gif}")
return
Set up Animation for Camera and Key Frames
Set up the animation configuration
set_animation_parameters(name='Camera', frame_start=1, frame_end=10, x_start=-300, y_start=100, z_start=200, x_move_speed=0, y_move_speed=-100, z_move_speed=0)
set_animation_parameters
set the animation configuration
Parameters: |
|
---|
Source code in bw/bl/set_animation_parameters.py
def set_animation_parameters(
name: str = "Camera",
frame_start: int = 1,
frame_end: int = 10,
x_start: int = -300,
y_start: int = 100,
z_start: int = 200,
x_move_speed: int = 0,
y_move_speed: int = -100,
z_move_speed: int = 0,
):
"""
set_animation_parameters
set the animation configuration
:param name: name of the camera
bpy.data.objects[name]
:param frame_start: start frame index
:param frame_end: end frame index
:param x_start: camera start x
:param y_start: camera start y
:param z_start: camera start z
:param x_move_speed: camera x move speed
:param y_move_speed: camera x move speed
:param z_move_speed: camera x move speed
"""
bpy.context.scene.frame_start = frame_start
bpy.context.scene.frame_end = frame_end
# Create a new animation data block
animation = bpy.data.actions.new(name="CameraAnimation")
bpy.data.objects[name].animation_data_create()
bpy.data.objects[name].animation_data.action = animation
# Adjust the camera clipping distance
bpy.data.objects[
name
].data.clip_start = 0.1 # Set the near clipping distance (adjust as needed)
# < 64x64 the clipping distance will show more
bpy.data.objects[
name
].data.clip_end = 600.0 # Set the far clipping distance (adjust as needed)
# Set other camera properties if necessary
bpy.data.objects[
name
].data.lens = 25.0 # Adjust the focal length if needed
bpy.context.scene.frame_set(
bpy.context.scene.frame_start
)
# Move the camera in the x axis
bpy.data.objects[name].location.x = x_move_speed
# Move the camera in the y axis
bpy.data.objects[name].location.y += y_move_speed
# Move the camera in the z axis
bpy.data.objects[name].location.z = z_move_speed
bpy.data.objects[name].keyframe_insert(
data_path="location", index=1
)
# Create a new keyframe for the camera location and rotation
bpy.context.scene.frame_set(bpy.context.scene.frame_end)
bpy.data.objects[name].location = (
x_start,
y_start,
z_start,
)
bpy.data.objects[name].keyframe_insert(
data_path="location", index=1
)
Set the Background Color for the Animation
set_background_color(use_alpha=False, world_r=0.0, world_g=0.0, world_b=0.0)
set_background_color
set the background color for easier animations
Parameters: |
|
---|
Source code in bw/bl/set_background_color.py
def set_background_color(
use_alpha: bool = False,
world_r: float = 0.0,
world_g: float = 0.0,
world_b: float = 0.0,
):
"""
set_background_color
set the background color for easier animations
:param use_alpha: flag to set the animations
as transparent
:param world_r: world decimal red value
:param world_g: world decimal green value
:param world_b: world decimal blue value
"""
bpy.context.scene.render.film_transparent = use_alpha
bpy.context.scene.view_settings.view_transform = (
"Standard"
)
bpy.context.scene.view_settings.look = "None"
bpy.context.scene.view_settings.exposure = 0.0
bpy.context.scene.view_settings.gamma = 0.1
bpy.context.scene.view_settings.view_transform = (
"Standard"
)
bpy.context.scene.world.use_nodes = False
"""
# does not work on 4.0, works on 3.0
bpy.context.scene.world.light_settings.use_ambient_occlusion = (
True
)
"""
# Set color in world
bpy.context.scene.world.color = (
world_r,
world_g,
world_b,
)
Set the Camera Orientation (Location and Direction)
set_camera_location_orientation(name='Camera', x_start=-300, y_start=100, z_start=200, x_end=-300, y_end=500, z_end=200, x_rotation=90, y_rotation=0, z_rotation=0)
set_camera_location_orientation
set up a named camera for navigating a path through blender by setting the start position (x, y, z) and end position (x, y, z) with an initial viewing angle (in radians for (x, y, z))
Parameters: |
|
---|
Source code in bw/bl/set_camera_location_orientation.py
def set_camera_location_orientation(
name: str = "Camera",
x_start: int = -300,
y_start: int = 100,
z_start: int = 200,
x_end: int = -300,
y_end: int = 500,
z_end: int = 200,
x_rotation: int = 90,
y_rotation: int = 0,
z_rotation: int = 0,
):
"""
set_camera_location_orientation
set up a named camera for navigating
a path through blender by
setting the start position (x, y, z) and
end position (x, y, z) with an initial
viewing angle (in radians for (x, y, z))
:param name: camera name in
bpy.data.objects[name]
:param x_start: camera start x position
:param y_start: camera start y position
:param z_start: camera start z position
:param x_end: camera end x position
:param y_end: camera end y position
:param z_end: camera end z position
:param x_rotation: camera rotation angle
for viewing in math.radians(x_rotation)
:param y_rotation: camera rotation angle
for viewing in math.radians(y_rotation)
:param z_rotation: camera rotation angle
for viewing in math.radians(z_rotation)
"""
# Create a look at target
bpy.data.objects[name].location = (
x_start,
y_start,
z_start,
)
look_at_target = bpy.data.objects.new(
"LookAtTarget", None
)
bpy.context.collection.objects.link(look_at_target)
look_at_target.location = (x_end, y_end, z_end)
# Set up the camera constraints
log.debug(
f"setting camera={name} "
f"start=({x_start}, {y_start}, {z_start}) "
f"look_at=({x_end}, {y_end}, {z_end}) "
f"end=({x_end}, {y_end}, {z_end}) "
f"angle=({x_rotation}, {y_rotation}, {z_rotation})"
)
bpy.context.scene.camera = bpy.data.objects[name]
track_constraint = bpy.data.objects[
name
].constraints.new("TRACK_TO")
track_constraint.target = look_at_target
track_constraint.track_axis = "TRACK_NEGATIVE_Z"
track_constraint.up_axis = "UP_Y"
bpy.data.objects[name].rotation_euler = (
math.radians(x_rotation),
math.radians(y_rotation),
math.radians(z_rotation),
)
Set the Output File for the Animation
set_output_file(filepath='./.tmp/temp_frame_', file_format='RBGA')
set_output_file
set the animation temporary file output paths
Parameters: |
|
---|
Source code in bw/bl/set_output_file.py
def set_output_file(
filepath: str = "./.tmp/temp_frame_",
file_format: str = "RBGA",
):
"""
set_output_file
set the animation temporary file output paths
:param filepath: path to save the temp files
:param file_format: format to use when
saving files during the animation
"""
bpy.context.scene.render.filepath = filepath
bpy.context.scene.render.image_settings.file_format = (
file_format
)
Set the Camera and World Render Settings
set_render_settings(file_format='PNG', color_mode='RGB', color_depth='8', use_zbuffer=False, color_alpha=False)
set_render_settings
set common animation rendering properties like file format, color mode, color depth, and to use the z buffer
Parameters: |
|
---|
Source code in bw/bl/set_render_settings.py
def set_render_settings(
file_format: str = "PNG",
color_mode: str = "RGB",
color_depth: str = "8",
use_zbuffer: bool = False,
color_alpha: bool = False,
):
"""
set_render_settings
set common animation rendering properties
like file format, color mode, color depth,
and to use the z buffer
:param file_format: type of animation image
:param color_mode: RBG vs RBGA
:param color_depth: '8' or '16' ('32' not
supported)
:param use_zbuffer: flag to use the z buffer
:param color_alpha: flag to use alphaa
"""
bpy.context.scene.render.image_settings.file_format = (
file_format
)
bpy.context.scene.render.image_settings.color_mode = (
color_mode
)
bpy.context.scene.render.image_settings.color_depth = (
color_depth
)
# supported on 3.0, not 4.0
"""
bpy.context.scene.render.image_settings.use_zbuffer = (
use_zbuffer
)
"""
Coloring APIs
Colors are disabled when rendering models with greater than 2000 faces.
Assign Colors based off Quantile Ranges
get_quantile_colors(z_values, min_value, max_value, num=10, opacity=0.5, color_map=None)
get_quantile_colors
PROMPTS
build a color dictionary for quickly mapping weights to a color based off quantile ranges in the z_values
Parameters: |
|
---|
Source code in bw/bl/get_quantile_colors.py
def get_quantile_colors(
z_values,
min_value: float,
max_value: float,
num: int = 10,
opacity: float = 0.5,
color_map: dict = None,
):
"""
get_quantile_colors
PROMPTS
build a color dictionary for
quickly mapping weights to a color
based off quantile ranges in the z_values
:param z_values: numpy 2d array
:param min_value: start
value for dynamic quantile calculations
using the num argument
:param max_value: end
value for dynamic quantile calculations
using the num argument
:param num: number of quantiles to
map to colors (it does not have to
be all of the colors either)
:param opacity: transparency
:param color_map: optional - existing
color map to use
with default from the
bw.bl.colors.get_color_tuples() api
"""
# Calculate quantile intervals
color_map = calc.calculate_weighted_quantile_ranges_2d(
array_2d=z_values, num_ranges=num + 1
)
return color_map
Color Map
get_color_tuples(opacity=0.5)
get_color_tuples
build common colorization tuples for dynamic shapes and return the colors and a dictionary for reference later.
colors are implemented as rgb decimals
Parameters: |
|
---|
Source code in bw/bl/colors.py
def get_color_tuples(
opacity: float = 0.5,
):
"""
get_color_tuples
build common colorization tuples
for dynamic shapes and return
the colors and a dictionary
for reference later.
colors are implemented as rgb decimals
:param opacity: apply a consistent opacity
for all colors to start
with default set to 0.5
"""
color_map = {
"red": (1.0, 0.0, 0.0, opacity), # color = red
"light red": (
1.0,
0.5,
0.5,
opacity,
), # color = light red
"dark brown": (
0.37,
0.18,
0.00,
opacity,
), # color = dark brown
"brown": (
0.64,
0.32,
0.00,
opacity,
), # color = brown
"orange": (
1.00,
0.50,
0.00,
opacity,
), # color = orange
"light orange": (
1.00,
0.74,
0.03,
opacity,
), # color = light orange
"light green": (
0.71,
1.00,
0.04,
opacity,
), # color = light green
"green": (
0.00,
1.00,
0.00,
opacity,
), # color = green
"dark green": (
0.32,
0.37,
0.00,
opacity,
), # color = dark green
"neon green": (
0.20,
0.37,
0.01,
opacity,
), # color = neon green
"myrtle": (
0.12,
0.22,
0.00,
opacity,
), # color = myrtle
"lincoln green": (
0.20,
0.37,
0.01,
opacity,
), # color = lincoln green
"darker green": (
0.01,
0.22,
0.04,
opacity,
), # color = darker green
"dark green-blue": (
0.01,
0.22,
0.13,
opacity,
), # color = dark green-blue
"blue": (0.01, 0.16, 0.22, opacity), # color = blue
"light blue": (
0.02,
0.28,
0.39,
opacity,
), # color = light blue
"dark blue": (
0.01,
0.14,
0.39,
opacity,
), # color = dark blue
"light purple": (
0.11,
0.02,
0.36,
opacity,
), # color = light purple
"purple": (
0.31,
0.03,
0.37,
opacity,
), # color = purple
"yellow": (
1.0,
1.0,
0.0,
opacity,
), # color = yellow
}
return color_map
Export Scene to Other Formats
Save as STL
Note: GitHub can automatically show small STL files.
export STL=./blender/example-model.stl
save_as_stl(output_path)
save_as_stl
save the scene as an stl file
https://docs.blender.org/api/current/bpy.ops.ex port_mesh.html#module-bpy.ops.export_mesh
Parameters: |
|
---|
Source code in bw/bl/save_as_stl.py
def save_as_stl(
output_path: str,
):
"""
save_as_stl
save the scene as an stl file
https://docs.blender.org/api/current/bpy.ops.ex
port_mesh.html#module-bpy.ops.export_mesh
:param output_path: save the scene
as an stl file at this local file path
"""
log.info(f"saving stl={output_path}")
bpy.ops.export_mesh.stl(
filepath=output_path,
# single file
batch_mode="OFF",
# use_selection=True
)
Save as glTF
export GLTF=./blender/example-model-gltf
save_as_gltf(output_path, export_format='GLB')
save_as_gltf
save the scene as a gltf file
https://docs.blender.org/api/current/bpy.ops. export_scene.html#bpy.ops.export_scene.gltf
Output Format
- GLB - glTF Binary (.glb)
Exports as a single file, with all data packed in binary form. Most efficient and portable, but more difficult to edit later.
- GLTF_SEPARATE - glTF separate files (.gltf + .bin + textures)
Exports multiple files, with separate JSON, binary and texture data. Easiest to edit later.
Parameters: |
|
---|
Source code in bw/bl/save_as_gltf.py
def save_as_gltf(
output_path: str,
export_format: str = "GLB",
):
"""
save_as_gltf
save the scene as a gltf file
https://docs.blender.org/api/current/bpy.ops.
export_scene.html#bpy.ops.export_scene.gltf
**Output Format**
- **GLB** - glTF Binary (.glb)
Exports as a single file, with all data packed
in binary form. Most efficient and portable, but
more difficult to edit later.
- **GLTF_SEPARATE** - glTF separate files (.gltf + .bin + textures)
Exports multiple files, with separate JSON,
binary and texture data. Easiest to edit later.
:param output_path: save the scene
as a gltf file at this local file path
:param export_format: use a single file
with GLB and multiple files with
GLTF_SEPARATE
"""
log.info(
f"saving gltf={output_path} "
f"format={export_format}"
)
bpy.ops.export_scene.gltf(
filepath=output_path,
export_format=export_format,
export_animations=True,
)