Blender 3.0 using conventional mode:
using top bar to switch Layout, Modeling, Sculpting, ... workview
using 1
, 2
, 3
, ... to switch mode (4
is object mode, other mode only modify selected object in 4
)
using Q
, W
, E
, ... to switch tool
Modifier: non-destructive (unless you click apply
) nodes apply to a mesh
Rendering: right top surfaces. wire frame allows you to select through model
Perspective: right gizmo
Other Tools: (using TAB
to search)
shrink/flatten: expand or shrink along normal
weight paint mode (7
)
Side Panel:
Render: Ctrl
+ Enter
Evee: online realtime render
Cycles: offline PRB render
Geometry Nodes:
Distribute Point of Face
Cylinder
RotateEuler
Instance on Point
Join Geometry
Math
Ctrl
+ G
: join nodes
Combine XYZ
Collection Info
: add a set of items to graph
Advanced Techniques:
Shader Nodes: (remember to click use node
)
TextureCoordinate
: give noise texture UV (Select object
if object generated in Blender)
NoiseTexture
: noise texture
ColorRamp
: add after noise texture
Ctrl
+ Shift
+ Left Click
(with Node Wrangler
plugin ): view current modified
White Noise Texture
: generate new random based on original random
Object Info
: get object info, including random
The Principal BSDF Shader: shader of all shaders
It take account of Fresnel: object with normal perpendicular to eye sight has more reflection
Specular: always 1.0
, how much reflection. Don't change it unless for non-physical rendering. Change Roughness instead.
Roughness: less overall Fresnel with rough surface, automatic adjust Fresnel (on edge) while adjusting roughness
Metallic: usually either 0
or 1
Subsurface Scattering (Subsurface): light go through model
Anisotropic: stretch reflection
Sheen: for fabric, turn off Specular and adjust Sheen
Clearcoat: add a transparent coat to object; combine rough reflection and smooth reflection
Transmission/IOR: related to glass shader
Short Cuts:
A
: focus to entire scene
X
(Snap): snap (or magnite bar on top)
B
(Proportional Editing Object): enable smooth editing around selected (or circle dot on top)
F
: focus to object
Tricks:
Sculpting:
SHIFT
): smoothG
): grabD
): drawI
): inflate (set stroke to aribrush
(building on top) or space
(automatically adding dots as you move far)): inflateMMD Tutorial:
3D22D: Demo
Subdivision: subdivide Solidify: expand, shrink Shrinkwrap: trying to move every vertex to the center until touching other surface
Device: GPU Sampling: 128 + Denoise Max Bounce: (Frame 1055; 46.40; 4210M)
Diffuse: 1
Glossy: 1
Transparent: 4 Caustic: None (Frame 1055; 46.15; 4210M) Tailing: 1024*8 (Frame 1055; 42.63; 4210M) Hair BVM: disable (Frame 1055; 48.79; 4210M) Persistent Data: enable (but may crash) Render and Compose Workflow: ... Optimize Texture: (Frame 1055; 01:25.22; 01:04.22; 4210M) Experimental Subdivision: Offscreen Scale = 64 (Frame 1055; 01:11.90; 4210M) Background Render: (Frame 187; 00:24.00; 4194M)
// 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:
auto_load.py
: helps you to register all modules
camera.py
: helper function I defined
__init__.py
: call auto_load.py
and register stuff. It provides plugin's metadata. It also help register global or local (operators-owned) variables that ties to an operation. The reason why operators need to own variables is that blender would like users to change variables back-and-forth to test the operation's outcome (for example, dynamically changing the size of cube you are generating)
operators.py
: defines blender operation to do stuff (e.g. edit geometry)
sampling.py
: helper function I defined
ui.py
: create blender UI that can set global variables or tie to an operator
util.py
: helper function I defined
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:
bpy.types.Scene
: global variable stored with .blend
file
bpy.types.WindowManager
: global variable that disappear after re-launch
bpy.types.Object
: per-object variable that is different for every objet currently selecting
For other namesapce, check here
The auto_load.py
looks like the following;
If you don't use
auto_load.py
, you would have to manually write a bunch ofbpy.utils.register_class()
andbpy.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
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
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
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.
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, especially come to scene matrices, are extremely unintuitive to work with. Some bugs are often "features" considered by Blender developer. For example:
Camera matrix will not be updated when moving camera: Stackoverflow
When copying a matrix using mat = camera.matrix_world.copy()
, without copy()
you will modify the matrix.
Coordinate differs from standard convention: Github Issue
You can do things that fix textures:
convert resource links to relative path
convert resource links to absolute path
bundle all outside sources into .blend
file
let blender automatically tell you which texture is missing
let blender search and fix textures for you automatically
fix textures manually
// TODO: write this section
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
:
mist-pass
is normalized value in [0, 1]mist-pass
is antialiased. The z-pass
is not.Scene > World > Mist Pass
Table of Content