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:
  • model_file (str, default: None ) –

    path to model.safetensors file and uses the MODEL environment variable (e.g. export MODEL="./model.safetensors")

  • num_gen (int, default: None ) –

    number of generations to run the training visualization through and uses the NUM_GEN environment variable (e.g. export NUM_GEN=5)

  • target_faces (int, default: None ) –

    number of polygon faces to extract and draw per tensor weight and uses the FACES environment variable (e.g. export FACES=20000)

  • target_rows (int, default: None ) –

    number of rows (width) for the 3d array in blender and the gif and uses the ROWS environment variable (e.g. export ROWS=256)

  • target_cols (int, default: None ) –

    number of columns (height) for the 3d array in blender and the gif and uses the COLS environment variable (e.g. export COLS=256)

  • layer_names (str, default: None ) –

    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.")

  • max_layers (int, default: None ) –

    number of layers to display in blnder and the gif and uses the MAX_LAYERS environment variable (e.g. export MAX_LAYERS=5)

  • save_gif (str, default: None ) –

    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")

  • save_gltf (str, default: None ) –

    optional - path to save the blender scene as a glTF file

  • save_stl (str, default: None ) –

    optional - path to save the blender scene as an STL file

  • num_frames (int, default: None ) –

    number of frames for the animation and uses the FRAMES environment variable (e.g. export FRAMES=5)

  • output_dir (str, default: None ) –

    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")

  • decimation_ratio (float, default: None ) –

    reduce each layer this percentage with supported values between 0.0 and 1.0

  • shutdown (bool, default: None ) –

    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

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:
  • input_file (str) –

    path to model.safetensors file

  • layer_names (list, default: [] ) –

    filter by layer layer colomn names

  • max_layers (int, default: 30 ) –

    limit the number of layers to render

  • device (str, default: 'cpu' ) –

    cpu vs gpu

  • target_faces (int, default: None ) –

    min faces to hopefully render if there is enough data

  • target_rows (int, default: 256 ) –

    number of resample target rows before drawing

  • target_cols (int, default: 256 ) –

    number of resample target cols before drawing

  • x (int, default: 0 ) –

    starting x axis location for all shapes

  • y (int, default: 0 ) –

    starting y axis location for all shapes

  • z (int, default: 1 ) –

    starting z axis location for all shapes

  • rotate_z (float, default: None ) –

    starting z euler rotation in radians

  • max_depth (int, default: 2 ) –

    stack each layer on itself this many times to convert it to a 3d ndarray

  • pad_per (int, default: 20 ) –

    number to pad per object

  • save_gif (str, default: None ) –

    optional - path to save the fly-through camera animation as a single gif

  • save_gltf (str, default: None ) –

    optional - path to save the blender scene as a glTF file

  • save_stl (str, default: None ) –

    optional - path to save the blender scene as an STL file

  • output_dir (str, default: None ) –

    save outputs to this dir

  • num_frames (int, default: 10 ) –

    number of animation key frames to show before saving a gif

  • decimation_ratio (float, default: None ) –

    reduce each layer this percentage with supported values between 0.0 and 1.0

  • shutdown_after_animation (bool, default: False ) –

    flag to shutdown the blender ui application after saving the animation

  • auto_convert (bool, default: True ) –

    flag to run ImageMagick to convert all temp animation images to a gif if set to True

  • file_format (str, default: 'PNG' ) –

    type of file for temp animation images ('PNG' vs 'TIFF' vs 'JPEG')

  • color_depth (str, default: '8' ) –

    '8' bit vs '16' bit

  • color_mode (str, default: 'RGBA' ) –

    'RGBA' vs 'RBG'

  • center_camera (bool, default: True ) –

    flag to center the camera for animations based off the target dimensions

  • animation_y_move_speed (int, default: -200 ) –

    how fast does the camera fly during the animation

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:
  • data (ndarray) –

    3d array data

  • target_faces (int, default: None ) –

    optional - find the nearest marching cubes configuration by resulting number of faces in the volume

  • mask (ndarray, default: None ) –

    3d array for mask

  • name (str, default: None ) –

    optional - name for labeling the mesh to the left

  • name_color (str, default: '#FFFFFF' ) –

    hex color string for the name text

  • name_opacity (float, default: 1.0 ) –

    opacity for the name text

  • name_extrude (float, default: 1.7 ) –

    extusion amount for the name text

  • desc (str, default: None ) –

    optional - description for labeling the mesh to the left

  • desc_color (str, default: '#555555' ) –

    hex color string for the desc text

  • desc_opacity (float, default: 1.0 ) –

    opacity for the desc text

  • desc_extrude (float, default: 1.5 ) –

    extusion amount for the name text

  • x (float, default: 0.0 ) –

    x position for the mesh

  • y (float, default: 0.0 ) –

    y position for the mesh

  • z (float, default: 0.0 ) –

    z position for the mesh

  • level (float, default: 1.0 ) –

    level for the marching cubes algorithm

  • vertex_scale_size (float, default: 1.0 ) –

    apply a scaler to each vertex

  • color_percentile (float, default: 95.0 ) –

    target percentile for applying a color to a cube face

  • background_enabled (bool, default: False ) –

    flag for including drawing a rectangle under the mesh (and optional text)

  • background_color (str, default: '#000000' ) –

    hex color string with default #000000 for black

  • background_opacity (float, default: 1.0 ) –

    transparency for the background with default no transparency at 1.0

  • background_height (int, default: None ) –

    how tall is the background

  • background_width (int, default: None ) –

    how wide is the background

  • background_depth (int, default: None ) –

    how deep is the background

  • background_x (float, default: None ) –

    x position for the background

  • background_y (float, default: None ) –

    y position for the background

  • background_z (float, default: None ) –

    z position for the background

  • step_size (int, default: 1 ) –

    number of marching cube step sizes

  • clean_workspace (bool, default: False ) –

    flag for deleting all objects before rendering

  • target_mb (float, default: None ) –

    optional - find the nearest marching cubes configuration by resulting megabyte size (more useful to use target_faces)

  • mc_report_file (str, default: None ) –

    optional - path to save the mc_report slim dictionary as a json file

  • num_colors (int, default: 5 ) –

    optional - number of different colors (too many colors will cause the mesh to fail drawing)

  • decimation_ratio (float, default: None ) –

    reduce each layer this percentage with supported values between 0.0 and 1.0

  • safe_for_colors_in_ram (bool, default: False ) –

    flag to force-enable colors. coloring this much data is very expensive so it is off by default

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:
  • percentile (float) –

    percentile that needs a color

  • color_dictionary (dict) –

    source of truth for how to colorize percentiles based off pre-existing setup (for more refer to bw.bl.get_percentile_colors)

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:
  • height (int) –

    height of the object

  • width (int) –

    width of the object

  • depth (int) –

    depth of the object

  • hex_color (str) –

    hex color string

  • opacity (float) –

    transparency

  • x_position (int) –

    x location

  • y_position (int) –

    y location

  • z_position (int) –

    z location

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:
  • text (str) –

    message to show in blender

  • position (tuple, default: (0, 0, 0) ) –

    (x, y, z) integer location for the text

  • font_size (int, default: 8 ) –

    size of the font

  • font_style (str, default: None ) –

    style for the font like BOLD

  • font_family_path (str, default: None ) –

    optional path to the font family file to use

  • color (str, default: '#FFFFFF' ) –

    hex color string for the text

  • opacity (float, default: 1.0 ) –

    alpha color for the text

  • extrude (float, default: 0.1 ) –

    amount to extend the text

  • bevel_depth (float, default: 0.05 ) –

    amount to curve the text edges

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:
  • hex_color (str) –

    hexidecimal color string

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:
  • name (str) –

    name of the object to decimate

  • decimation_ratio (float, default: 0.5 ) –

    percent to reduce the object between 0.0 and 1.0

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:
  • active_object

    set this to the blender active object to decimate (reduce) active_object=bpy.context.active_object

  • reduce_percentage (float, default: 0.5 ) –

    set the target reduction percentage as a value between 0.0 and 1.0

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:
  • save_gif (bool, default: False ) –

    flag to enable saving the animation as a gif

  • gif_path (str, default: None ) –

    path to save the gif

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:
  • save_gif (str) –

    path to save the animation as a gif file

  • center_camera (bool, default: True ) –

    flag to center the camera for animations based off the target dimensions

  • target_rows (int, default: None ) –

    number of rows for camera centering

  • target_cols (int, default: None ) –

    number of columns for camera centering

  • output_dir (str, default: None ) –

    temp directory for saving animation images before gif compilation

  • frame_start (int, default: 1 ) –

    start frame idx usually 1

  • frame_end (int, default: 2 ) –

    ending frame idx

  • x_start (int, default: -300 ) –

    camera start x position

  • y_start (int, default: 0 ) –

    camera start y position

  • z_start (int, default: 200 ) –

    camera start z position

  • x_end (int, default: -300 ) –

    camera end z position

  • y_end (int, default: 500 ) –

    camera end z position

  • z_end (int, default: 200 ) –

    camera end z position

  • x_move_speed (int, default: 0 ) –

    speed the camera navigates from the x_start to the x_end position

  • y_move_speed (int, default: -10 ) –

    speed the camera navigates from the y_start to the y_end position

  • z_move_speed (int, default: 0 ) –

    speed the camera navigates from the z_start to the z_end position

  • shutdown_after_animation (bool, default: True ) –

    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.

  • auto_convert (bool, default: True ) –

    flag to run ImageMagick to convert all temp animation images to a gif if set to True

  • file_format (str, default: 'PNG' ) –

    type of file for temp animation images ('PNG' vs 'TIFF' vs 'JPEG')

  • color_depth (str, default: '8' ) –

    '8' bit vs '16' bit

  • color_mode (str, default: 'RGBA' ) –

    '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.

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:
  • name (str, default: 'Camera' ) –

    name of the camera bpy.data.objects[name]

  • frame_start (int, default: 1 ) –

    start frame index

  • frame_end (int, default: 10 ) –

    end frame index

  • x_start (int, default: -300 ) –

    camera start x

  • y_start (int, default: 100 ) –

    camera start y

  • z_start (int, default: 200 ) –

    camera start z

  • x_move_speed (int, default: 0 ) –

    camera x move speed

  • y_move_speed (int, default: -100 ) –

    camera x move speed

  • z_move_speed (int, default: 0 ) –

    camera x move speed

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:
  • use_alpha (bool, default: False ) –

    flag to set the animations as transparent

  • world_r (float, default: 0.0 ) –

    world decimal red value

  • world_g (float, default: 0.0 ) –

    world decimal green value

  • world_b (float, default: 0.0 ) –

    world decimal blue value

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:
  • name (str, default: 'Camera' ) –

    camera name in bpy.data.objects[name]

  • x_start (int, default: -300 ) –

    camera start x position

  • y_start (int, default: 100 ) –

    camera start y position

  • z_start (int, default: 200 ) –

    camera start z position

  • x_end (int, default: -300 ) –

    camera end x position

  • y_end (int, default: 500 ) –

    camera end y position

  • z_end (int, default: 200 ) –

    camera end z position

  • x_rotation (int, default: 90 ) –

    camera rotation angle for viewing in math.radians(x_rotation)

  • y_rotation (int, default: 0 ) –

    camera rotation angle for viewing in math.radians(y_rotation)

  • z_rotation (int, default: 0 ) –

    camera rotation angle for viewing in math.radians(z_rotation)

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:
  • filepath (str, default: './.tmp/temp_frame_' ) –

    path to save the temp files

  • file_format (str, default: 'RBGA' ) –

    format to use when saving files during the animation

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:
  • file_format (str, default: 'PNG' ) –

    type of animation image

  • color_mode (str, default: 'RGB' ) –

    RBG vs RBGA

  • color_depth (str, default: '8' ) –

    '8' or '16' ('32' not supported)

  • use_zbuffer (bool, default: False ) –

    flag to use the z buffer

  • color_alpha (bool, default: False ) –

    flag to use alphaa

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:
  • z_values

    numpy 2d array

  • min_value (float) –

    start value for dynamic quantile calculations using the num argument

  • max_value (float) –

    end value for dynamic quantile calculations using the num argument

  • num (int, default: 10 ) –

    number of quantiles to map to colors (it does not have to be all of the colors either)

  • opacity (float, default: 0.5 ) –

    transparency

  • color_map (dict, default: None ) –

    optional - existing color map to use with default from the bw.bl.colors.get_color_tuples() api

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:
  • opacity (float, default: 0.5 ) –

    apply a consistent opacity for all colors to start with default set to 0.5

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:
  • output_path (str) –

    save the scene as an stl file at this local file path

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:
  • output_path (str) –

    save the scene as a gltf file at this local file path

  • export_format (str, default: 'GLB' ) –

    use a single file with GLB and multiple files with GLTF_SEPARATE

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,
    )