Blender

Blender Introduction

Blender 3.0 using conventional mode:

Other Tools: (using TAB to search)

Side Panel:

Render: Ctrl + Enter

Geometry Nodes:

Advanced Techniques:

Shader Nodes: (remember to click use node)

Proper Roughness with Automatic Fresnel

Proper Roughness with Automatic Fresnel

The Principal BSDF Shader: shader of all shaders

Short Cuts:

Tricks:

Sculpting:

MMD Tutorial:

3D22D: Demo

Modifier

Subdivision: subdivide Solidify: expand, shrink Shrinkwrap: trying to move every vertex to the center until touching other surface

Reduce Rendering Time

Device: GPU Sampling: 128 + Denoise Max Bounce: (Frame 1055; 46.40; 4210M)

Writing Blender Plugins

// TODO: write this section

Installed scripts is in /home/koke_cacao/.config/blender/3.1/scripts/addons/. You may be able to directly edit in that directory.

You can do the following to link addons

cd ~/.config/blender/3.1/scripts/addons && \
ln -s ~/Documents/Koke_Cacao/Blender/Addon/instant_ngp/instant-ngp instant-ngp

Using this setup, you only need to restart blender every time you make a change of your addon.

It is always a good safety practice to uninstall and close blender before re-install plugins for manual installation

A typical plugin look like this

project
├── auto_load.py
├── camera.py
├── __init__.py
├── operators.py
├── __pycache__
├── sampling.py
├── ui.py
└── util.py

To "compile" it, you just need to package them into a zip file using the command zip project.zip -r project README.md

Here is what each file does:

init.py

You should declear properties about your plugin:

bl_info = {
    "name" : "instant-ngp",
    "author" : "Koke_Cacao",
    "description" : "An Realtime NGP Generator",
    "blender" : (2, 80, 0),
    "version" : (1, 0, 1),
    "location" : "View3D",
    "warning" : "",
    "category" : "Generic"
}

If you want to automatically load every Operators and UI with auto_load.py, then also put the following in your __init__.py

from . import auto_load
import bpy
import math

auto_load.init()

def register():
  auto_load.register()

def unregister():
  auto_load.unregister()

Under register() and unregister(), you can also initialize and remove your global variables:

namespace = bpy.types.Scene

def register:
  namespace.ngp_frame_from = bpy.props.IntProperty(name="Frame From", description="...", default=1, min=1)
  namespace.ngp_use_current_frame = bpy.props.BoolProperty(name="Use Current Frame", description="...", default=True)
  namespace.ngp_camera = bpy.props.StringProperty(name="Camera", description="...", default='value')
  namespace.ngp_bounding_box = bpy.props.StringProperty(name="Bounding Box", description="...", default='value')
  namespace.ngp_rho_min = bpy.props.FloatProperty(name="Rho Min", description="rho min", default=1.0, min=epsilon)
  namespace.ngp_sampling_fn = bpy.props.EnumProperty(
      items=(
          ('HAMMERSLEY', "Hammersley", ""),
          ('UNIFORM', "Uniform", ""),
          ('DETERMINISTIC', "Deterministic", ""),
          ('STRATIFIED', "Stratified", ""),
      ),
      default='HAMMERSLEY'
  )
  auto_load.register()

def unregister:
  del namespace.ngp_frame_from
  # ... and more ...
  auto_load.unregister()

It is important to remove variables when unregister because client can unload and reload a plugin anytime during the use of blender.

Operator-owned variables is tied to an object and is stored in the .blend file. Some global variables is tied to the blender file, others are tied to the specific instance of blender (not stored, will lose settings after re-launch blender)

There are a few location you can store non-operators' variables:

The auto_load.py looks like the following;

If you don't use auto_load.py, you would have to manually write a bunch of bpy.utils.register_class() and bpy.utils.unregister_class() in those two functions.

import os
import bpy
import sys
import typing
import inspect
import pkgutil
import importlib
from pathlib import Path

__all__ = (
    "init",
    "register",
    "unregister",
)

blender_version = bpy.app.version

modules = None
ordered_classes = None


def init():
    global modules
    global ordered_classes

    modules = get_all_submodules(Path(__file__).parent)
    ordered_classes = get_ordered_classes_to_register(modules)


def register():
    for cls in ordered_classes:
        bpy.utils.register_class(cls)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "register"):
            module.register()


def unregister():
    for cls in reversed(ordered_classes):
        bpy.utils.unregister_class(cls)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "unregister"):
            module.unregister()


# Import modules
#################################################


def get_all_submodules(directory):
    return list(iter_submodules(directory, directory.name))


def iter_submodules(path, package_name):
    for name in sorted(iter_submodule_names(path)):
        yield importlib.import_module("." + name, package_name)


def iter_submodule_names(path, root=""):
    for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
        if is_package:
            sub_path = path / module_name
            sub_root = root + module_name + "."
            yield from iter_submodule_names(sub_path, sub_root)
        else:
            yield root + module_name


# Find classes to register
#################################################


def get_ordered_classes_to_register(modules):
    return toposort(get_register_deps_dict(modules))


def get_register_deps_dict(modules):
    my_classes = set(iter_my_classes(modules))
    my_classes_by_idname = {
        cls.bl_idname: cls
        for cls in my_classes if hasattr(cls, "bl_idname")
    }

    deps_dict = {}
    for cls in my_classes:
        deps_dict[cls] = set(
            iter_my_register_deps(cls, my_classes, my_classes_by_idname))
    return deps_dict


def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
    yield from iter_my_deps_from_annotations(cls, my_classes)
    yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname)


def iter_my_deps_from_annotations(cls, my_classes):
    for value in typing.get_type_hints(cls, {}, {}).values():
        dependency = get_dependency_from_annotation(value)
        if dependency is not None:
            if dependency in my_classes:
                yield dependency


def get_dependency_from_annotation(value):
    if blender_version >= (2, 93):
        if isinstance(value, bpy.props._PropertyDeferred):
            return value.keywords.get("type")
    else:
        if isinstance(value, tuple) and len(value) == 2:
            if value[0] in (bpy.props.PointerProperty,
                            bpy.props.CollectionProperty):
                return value[1]["type"]
    return None


def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
    if bpy.types.Panel in cls.__bases__:
        parent_idname = getattr(cls, "bl_parent_id", None)
        if parent_idname is not None:
            parent_cls = my_classes_by_idname.get(parent_idname)
            if parent_cls is not None:
                yield parent_cls


def iter_my_classes(modules):
    base_types = get_register_base_types()
    for cls in get_classes_in_modules(modules):
        if any(base in base_types for base in cls.__bases__):
            if not getattr(cls, "is_registered", False):
                yield cls


def get_classes_in_modules(modules):
    classes = set()
    for module in modules:
        for cls in iter_classes_in_module(module):
            classes.add(cls)
    return classes


def iter_classes_in_module(module):
    for value in module.__dict__.values():
        if inspect.isclass(value):
            yield value


def get_register_base_types():
    return set(
        getattr(bpy.types, name) for name in [
            "Panel",
            "Operator",
            "PropertyGroup",
            "AddonPreferences",
            "Header",
            "Menu",
            "Node",
            "NodeSocket",
            "NodeTree",
            "UIList",
            "RenderEngine",
            "Gizmo",
            "GizmoGroup",
        ])


# Find order to register to solve dependencies
#################################################


def toposort(deps_dict):
    sorted_list = []
    sorted_values = set()
    while len(deps_dict) > 0:
        unsorted = []
        for value, deps in deps_dict.items():
            if len(deps) == 0:
                sorted_list.append(value)
                sorted_values.add(value)
            else:
                unsorted.append(value)
        deps_dict = {
            value: deps_dict[value] - sorted_values
            for value in unsorted
        }
    return sorted_list

operators.py

Here is an example of operator that prints camera information

class Instant_NGP_PrintCamera_Op(Operator):
    bl_idname = "ngp.print_camera"
    bl_label = "ngp.print_camera"
    bl_description = "print debug camera info"

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        camera = bpy.data.objects[context.scene.ngp_camera]
        print_camera_info(camera)
        return {'FINISHED'}

An operator is a class that provides an execute and pull function. The execute function will tell blender what happen when a client want to "use" this operator. The pull function will tell blender when the execute function is avaliable for the client to use (usually non-null sanity checks).

bl_idname is the internal name of your operator, and bl_label is a human-readable name for your operator. bl_description is a human-readable description. All 3 variable must be set.

For example, a UI object can call execute() of this operator using the following code to print camera:

row = layout.row()
row.operator("ngp.print_camera", text="Print Camera")

Also, in execute(), a context variable is passed into the function. You can use that to retrieve context.object variables that ties to a selected object. To use global variable, you can do ngp_camera = context.scene.ngp_camera we set above in __init__.py.

Remember to return status code return {'FINISHED'} after successfully executing.

To define operator-specific variables, see stackoverflow

ui.py

You can specify UI element as follow. bl_space_type and bl_region_type specifies in what location should the UI be displayed at. bl_label and bl_category the main name of the UI. All four variables should be set.

class VIEW3D_PT_Instant_NGP_Panel(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_label = "Instant_NGP_Panel"
    bl_category = "Instant NGP"

    def draw(self, context):
        namespace = context.scene
        layout = self.layout

        # 1 col in 1 row
        row = layout.row()
        row.prop(namespace, "ngp_frame_from", expand=True)
        row.prop(namespace, "ngp_frame_to", expand=True)

        # 2 col in 1 row
        row = layout.row()
        col = row.column()
        col.prop(namespace, "ngp_camera", expand=True) # display and changing global variables
        col = row.column()
        col.operator("ngp.select_camera", text="Select Camera") # calling operator

Installing Python Libraries

Blender comes with its own version of python environment under blender path. For example, it might be located in /home/koke_cacao/.steam/steam/steamapps/common/Blender/3.1/python/bin. There are many ways to deal with it.

To force blender to use system python environment, launch blender with blender --python-use-system-env. This method might not always work, though.

You may also want to install libraries directly into blender's environment, do so by

cd /home/koke_cacao/.steam/steam/steamapps/common/Blender/3.1/python/bin && ./python -m pip install opencv-python

Or, you can load a library on the fly:

# check environment
checklist = [
    '/home/hankec/miniconda3/envs/ngp/lib/python3.10/site-packages/',
    '/home/koke_cacao/miniconda3/envs/ml/lib/python3.6/site-packages/'
]
# load the first one if avaliable
for path in checklist:
    if os.path.isdir(path):
        sys.path.append(path)
        print("loaded: {}".format(path))
        break

import cv2 # then import

Hint: type sys.executable in python shell can know what executable the python interactive shell is launched.

My Recommended Way

You could also make a symbolic link ln -s from blender's python directory to conda's directory. Note this method doesn't work well with automatic blender updates and might break things. When you update blender version, you need to do these steps again.

None of above is a perfect solution. Try them with caution.

The relyable method to use conda is as follow:

# check your Blender's python location using 'sys.executable'
cd /home/koke_cacao/.steam/steam/steamapps/common/Blender/3.1/
mv python _python
# check your Blender's python version using 'sys.version'
conda create --name=blender python=3.10.2
conda activate blender
# make sure you are still in the directory
sudo ln -s ~/miniconda3/envs/blender/ python
conda install numpy # Blender depends on numpy
PYTHONPATH=/home/koke_cacao/miniconda3/envs/blender/bin/python /home/koke_cacao/.steam/steam/steamapps/common/Blender/blender --python-use-system-env

Blender Gotchas

Blender, especially come to scene matrices, are extremely unintuitive to work with. Some bugs are often "features" considered by Blender developer. For example:

- For more gotchas see Here

Fixing Missing Textures

You can do things that fix textures:

Automatic Camera Orientation

// TODO: write this section

Depth Buffer for Point Clouds

You can enable z-pass and mist-pass in Scene > View Layer > Passes > Data section. After enabling, you go to Compose > Use Nodes and in Render Layers, all selected option will show. You can also enable viewport display in Camera > Cameras > Viewport Display > Show > Mist.

mist-pass is different from z-pass:

  1. instead of actual distance, the mist-pass is normalized value in [0, 1]
  2. mist-pass is antialiased. The z-pass is not.
  3. The range for the mist pass is set independently of the camera's clip point and can be set in Scene > World > Mist Pass

Table of Content