Difference between revisions of "HowTo:Use POV-Ray with Blender/Developers page"
m |
m (→C++) |
||
Line 486: | Line 486: | ||
− | But in Blender, '''all''' of the user interface is written in Python, that's why the file ''scripts/startup/bl_ui/space_text.py'' also | + | But in Blender, '''all''' of the user interface is written in Python, that's why the file ''scripts/startup/bl_ui/space_text.py'' also has been updated even though it lies outside the usual addons folder: |
<source lang="py"> | <source lang="py"> | ||
Line 869: | Line 869: | ||
</source> | </source> | ||
+ | |||
+ | Notice that the special command line | ||
+ | # <pep8-80 compliant> | ||
+ | writes pep-80 rather than pep-8 as getting closer to the native blender program often requires more demanding standards : pep-80 asks for even shorter line lengths and so a file passing the pep-80 automated tests that the Blender Foundation regularly runs might fail passing PEP-80. |
Revision as of 14:07, 29 July 2020
User's page | Developer's page |
- POV-Ray Site
- Exporter Quick Start
- POV-Ray Documentation
- Sample Materials
- POV-Ray centric default workspace
- ---
- ---
- ---
- ---
- ---
- ---
- ---
- ---
- design ideas
- regression files
The Blender to POV-Ray add-on and vice versa (aka "POV Converter") started out as a single Python file.
To be compliant with blender add-on packaging system this add-on had to:
- provide some general information about the add-on: its name and version,
- define some code to perform actions, mostly through 'operators' or 'functions'
- and make sure these are registered so that they can be used.
bpy
Blender has an embedded Python interpreter which is loaded when Blender is started and stays active while Blender is running. This interpreter runs scripts to draw the user interface and is used for some of Blender’s internal tools as well. Blender’s embedded interpreter provides a typical Python environment, so code from tutorials on how to write Python scripts can also be run with Blender’s interpreter. Blender provides its Python modules, such as bpy and mathutils, to the embedded interpreter so they can be imported into a script and give access to Blender’s data, classes, and functions. Scripts that deal with Blender data will need to import the modules to work. So of all modules, if no other, bpy is generally always imported in blender scripts. (except in interactive console were this import is automatically done for convenience)
bl_info
bl_info = {
"name": "Persistence of Vision",
"author": "Campbell Barton, "
"Maurice Raybaud, "
"Leonid Desyatkov, "
"Bastien Montagne, "
"Constantin Rahn, "
"Silvio Falcinelli",
"version": (0, 1, 1),
"blender": (2, 81, 0),
"location": "Render Properties > Render Engine > Persistence of Vision",
"description": "Persistence of Vision integration for blender",
"doc_url": "{BLENDER_MANUAL_URL}/addons/render/povray.html",
"category": "Render",
"warning": "Under active development, seeking co-maintainer(s)",
}
The general information about the add-on is defined in a dictionary with the name
bl_info
Each key of this dictionary provides Blender with specific information about the add-on although not all are equally important. Most of the information is used in the user preferences dialog and helps the user to find and select an add-on.
name
Kind of a code name for the addon, by which it is referenced in the Blender source... (UI name can show something else)
author
If you contribute you will be credited here !
version
The version of the add-on. Any numbering scheme is valid, as long as it is a tuple of three integers. Preferably only move the last position but we might choose for a more structured scheme later on.
blender
The minimal Blender version needed by this add-on. Again a tuple of three integers. Even if you expect things to work with older and nnewer versions it might be a good idea to list the earliest version that you actually tested your add-on with! Developing with something stable enough is important, and then test with latest version just before commit but don't generally update compatible version number then, only when something breaks at that stage.
category
The category in the user preferences our add-on is grouped under. It operates POV compatible engines which are renderers so it made sense to add it to the Render category historically. Unfortunately it doesnt reflect the multipurpose nature it ended up growing (such as shipped in importer, text editor syntax highlighting etc...) If several categories are possible maybe we should add them.
location
Where to find the add-on once it is enabled. This might a reference to a specific panel or inout case, a description of its location in a menu.
description
A concise description of what the add-on does.
warning
If this is not an empty string, the add-on will show up with a warning sign in the user preferences. You might use this to mark an add-on as experimental for example.
doc_url
the public on-line documentation, it is a derivative of the work happening here. It will be a click-able item in the user preferences.
tracker_url
The POV add-on being a bundled part of Blender it does have its own bug tracker entry associated with it and this key will provide a pointer to it.
Comments
Single line comments begin with a hash (#) character immediately followed by one blank space and the comment starting with a capital but not ending with a dot. They can be added after a short enough line of code (leave one blank space before the #).
For instance, at the beginning of the file, the comment
# <pep8 compliant>
means that one of the text formatting / coding style guideline to use is PEP8 so, for instance, you should take care to make short enough lines throughout all the script's files of less than 79 characters and less than 72 for docstrings (see below) or comments. Note that this very comment is a special syntax: Periodically the Blender Foundation runs checks for pep8 compliance on blender scripts, for scripts to be included in this check add this line as a comment at the top of the script.
Other rules include
- camel caps for class names: MyClass
- all lower case underscore separated module names: my_module
- indentation of 4 spaces (no tabs)
- spaces around operators. 1 + 1, not 1+1
- only use explicit imports, (no importing *)
- don’t use single line: if val: body, separate onto 2 lines instead.
While multi-line comments can begin with triple quotes. It is the convention that three double quotes are used for docstrings, a specific kind of comment not doing anything in the program but still appearing when console is used to get help about a function.
"""This is an explanation of general relevance"""
It is good practice to put a doc string at the start of every class to describe it and also at the start of every file to explain its relative use.
Functions
Functions are defined with the def keyword, followed by the name of the function and a pair of parenthesis. When the function is referenced to move it around and pass it along between files, it no longer needs the parenthesis, but pretty much every other time, they do follow function's name and in between them, some attributes can eventually be specified.
Classes
Not only do Classes allow you to create reusable definitions. But they are the only way to set up user interface elements in Blender. The good news is: that means addons developers actually have less work to do. The hardcoded implementation takes care of determining when and what parts to update for all ui elements automatically, and the inheritence of classes to choose from (the API) make sure one can handle situations in optimized ways. Some inherited functions such as draw() are to be overloaded by the add-on developer who rewrites each one to his need.
To create new classes, either use the regular class definition scheme as shown in the TextEditor Python Templates... Or copy and paste one from elsewhere in the script. Respect the same naming pattern as much as possible: OT means it's an operator, MT a menu, PT à panel, etc.
The Blender Python api allows integration for:
bpy.types.Panel
bpy.types.Menu
bpy.types.Operator
bpy.types.PropertyGroup
bpy.types.KeyingSet
bpy.types.RenderEngine
This is intentionally limited. Currently, for more advanced features such as mesh modifiers, object types, or shader nodes, C/C++ must be used.
For Python integration Blender defines methods which are common to all types. This works by creating a Python subclass of a Blender class which contains variables and functions specified by the parent class which are pre-defined to interface with Blender.
Note that we subclass a member of bpy.types, this is common for all classes which can be integrated with Blender and used so we know if this is an Operator and not a Panel when registering.
Both class properties start with a bl_ prefix. This is a convention used to distinguish Blender properties from those you add yourself.
Operators
Most add-ons define new operators, they are classes that implement specific functionality. The actual definition of the operator takes the form a class that is derived from bpy.types.Operator
import bpy
class MyTool(bpy.types.Operator):
"""Do things to an object"""
bl_idname = "object.do_things_to_object"
bl_label = "Do things to an object"
bl_options = {'REGISTER', 'UNDO'}
The docstring at the start of the class definition will be used as a tooltip anywhere this operator will be available, for example in a menu, while the bl_label defines the actual label used in the menu entry itself. Here we kept both the same. Operators will be part of Blender's data, and operators are stored in the module bpy.ops. This bl_idname will make sure this operator's entry will be called bpy.ops.object.move_object. Operators are normally registered in order to make them usable and that is indeed the default of bl_options. However, if we also want the add-on to show up in the history so it can be undone or repeated, we should add UNDO to the set of flags that is assigned to bl_options, as is done here.
Operators have limitations that can make them cumbersome to script.
Main limits are…
Can’t pass data such as objects, meshes or materials to operate on (operators use the context instead)
The return value from calling an operator gives the success (if it finished or was canceled), in some cases it would be more logical from an API perspective to return the result of the operation.
Operators poll function can fail where an API function would raise an exception giving details on exactly why.
The execute() function
An operator class can have any number of member functions but to be useful it normally overrides the execute() function:
def execute(self, context):
context.active_object.rotation.z += 33
return {'FINISHED'}
The execute() function is passed a reference to a context object. This context object contains among other things an active_object attribute which points to, Blenders active object. Each object in Blender has a rotation attribute which is a vector with x, y and z components. Changing the rotation of an object is as simple as changing one of these components, which is exactly what we do in line 2.
The execute() function signals successful completion by returning a set of flags, in this case a set consisting solely of a string FINISHED.
Defining an operator is not in itself enough to make this operator usable. In order for the user to find and use an operator, for example by pressing SPACE in the 3D view window and typing the label of the operator, we must register the operator. Adding a registered operator to a menu requires a separate action.
def register():
bpy.utils.register_module(__name__)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.types.VIEW3D_MT_object.remove(menu_func)
def menu_func(self, context):
self.layout.operator(MyTool.bl_idname, icon='MESH_CUBE')
When we check the Enable an add-on check-box in the user preferences, Blender will look for a register() function and execute it. Likewise, when disabling an add-on the unregister() function is called. Here we use this to both register our operator with Blender and to insert a menu entry that refers to our operator. The bpy.utils.register_module() function will register any class in a module that has REGISTER defined in its bl_options set. In order to create a menu entry we have to do two things:
- Create a function that will produce a menu entry
- And append this function to a suitable menu.
Now almost everything in Blender is available as a Python object and menus are no exception. We want to add our entry to the Object menu in the 3D view so we call
bpy.types.VIEW3D_MT_object.append()
and pass it a reference to the function we define a little below. How do we know how this menu object is called? If you have checked File⇒ User preferences ⇒ Interface ⇒ Python Tooltips the name of the menu will be shown in a tooltip when you hover over a menu.From the image above we can see that we can use bpy.types.VIEW3D_MT_object.append() to add something to the Object menu because VIEW3D_MT_object is shown in the balloon text. Note that the menu_func() function does not implement an action itself but will, when called, append a user interface element to the object that is passed to it in the self parameter. This user interface element in turn will interact with the user. Here we will simply add an operator entry (that is, an item that will execute our operator when clicked). The self argument that is passed to menu_func() refers to the menu. This menu has a layout attribute with an operator() function to which we pass the name of our operator. This will ensure that every time a user hovers over the Object menu, our operator will be shown in the list of options. The name of our new MyTool operator can be found in its bl_idname attribute so that is why we pass MyTool.bl_idname.
The name of the entry and its tooltip is determined by looking at the bl_label and docstring defined in our MyTool class and the icon used in the menu is determined by passing an optional icon parameter to the operator() function. This may sound overly complicated but it makes it possible for example to show different things than just click-able entries in a menu for example to group several operators in a box.
Optimizing
Keep dot notation minimal
Replace its uselessly repeated lookups with aliases | |
The dot operator is an easy way to grab some class, data, method, whatever python object included in a wider hierarchy... But be aware that it is expensive, especially if used in a loop. |
When calling
bpy.data.objects["my_obj"]
Every dot dives one level deeper and the whole hierarchy is walked through up to it. You always need to do it at least once to reach the desired item, so it's fine to use that syntax when the reference is only made once, but above that, always prefer to avoid the whole lookup and rather give it an alias like
obj = bpy.data.objects["my_obj"]
and then you can still use it , handling occasional attributes like below example:
obj.location.z += 1
and all other available attributes, for your reading or writing operations. Define as many of these required aliases before entering a loop rather than inside of it. Imagine if your GPS always told you previous directions taken from starting point up to the next one at every turn !
Use try/except sparingly
The try statement is useful to save time writing error checking code.
However try is significantly slower than an if since an exception has to be set each time, so avoid using try in areas of your code that execute in a loop and runs many times.
There are cases where using try is faster than checking whether the condition will raise an error, so it is worth experimenting.
Separate python (*.py) files
The script was later split into a package (several files into a folder) using as few files as possible to keep the flow of data easier to understand. There were initially 3 main files:
- __init__.py
- ui.py
- render.py
However, some of them reaching over 10 000 lines for such an add-on was still too much. So it is currently tolerated that the biggest file stays below 7000 lines. Most probably the files could lose some weight, by removing deprecated 32 bits support and modularizing some of the code into a few more reusable functions. Here are the current files composing the add-on and their respective use:
__init__.py
Initialize properties. (Note the double underscores around the file name. this makes it the first file launched) defining the 'main' unit for a package; it also causes Python to treat the specific directory as a package. It is the unit that will be used when you call import render_povray (and render_povray is a directory).
ui.py
Provide property buttons for the user to set up variables values. Some notes to keep in mind when writing UI layouts:
UI code is quite simple. Layout declarations are there to easily create a decent layout. General rule here: If you need more code for the layout declaration, then for the actual properties, you do it wrong.
Example layouts:
layout()
The basic layout is a simple Top -> Bottom layout.
layout.prop() layout.prop()
layout.row()
Use row(), when you want more than 1 property in one line.
row = layout.row() row.prop() row.prop()
layout.column()
Use column(), when you want your properties in a column.
col = layout.column() col.prop() col.prop()
layout.split()
This can be used to create more complex layouts. For example you can split the layout and create two column() layouts next to each other. Don’t use split, when you simply want two properties in a row. Use row() for that.
split = layout.split()
col = split.column() col.prop() col.prop()
col = split.column() col.prop() col.prop()
Declaration names:
Try to only use these variable names for layout declarations:
row for a row() layout
col for a column() layout
split for a split() layout
flow for a column_flow() layout
sub for a sub layout (a column inside a column for example)
render.py
Translate geometry and UI properties (Blender and POV native) to the POV file
primitives.py
Display some POV native primitives in 3D view for input and output
shading.py
Translate shading properties to declared textures at the top of a pov file
nodes.py
Translate node trees to the pov file
df3.py
Render smoke to *.df3 files
update_files.py
Update new variables to values from older API. This file needs an update
Presets
Along these essential files also coexist a few additional libraries to help make Blender stand up to other Persistence of Vision compatible frontends such as povwin or QTPOV
Material (sss)
Radiosity
World
Light
04_(6000K)_2500W_HMI_(Halogen_Metal_Iodide).py
05_(4000K)_100W_Metal_Halide.py
06_(3200K)_100W_Quartz_Halogen.py
09_(5000K)_75W_Full_Spectrum_Fluorescent_T12.py
10_(4300K)_40W_Vintage_Fluorescent_T12.py
11_(5000K)_18W_Standard_Fluorescent_T8.py
12_(4200K)_18W_Cool_White_Fluorescent_T8.py
13_(3000K)_18W_Warm_Fluorescent_T8.py
14_(6500K)_54W_Grow_Light_Fluorescent_T5-HO.py
15_(3200K)_40W_Induction_Fluorescent.py
16_(2100K)_150W_High_Pressure_Sodium.py
17_(1700K)_135W_Low_Pressure_Sodium.py
18_(6800K)_175W_Mercury_Vapor.py
22_(30000K)_40W_Black_Light_Fluorescent.py
templates
C++
Some areas could only be added POV specific functionality using C++ :
blender/editors/space_text/text_format_pov.c
blender/editors/space_text/text_format_ini.c
Mspace_text.c
Mtext_format.h
Mtext_format_ini.c
Mtext_format_pov.c
and the MCMakelists.txt had to be modified to include added files.
But in Blender, all of the user interface is written in Python, that's why the file scripts/startup/bl_ui/space_text.py also has been updated even though it lies outside the usual addons folder:
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
import bpy
from bpy.types import Header, Menu, Panel
from bpy.app.translations import pgettext_iface as iface_
class TEXT_HT_header(Header):
bl_space_type = 'TEXT_EDITOR'
def draw(self, context):
layout = self.layout
st = context.space_data
text = st.text
row = layout.row(align=True)
row.template_header()
TEXT_MT_editor_menus.draw_collapsible(context, layout)
if text and text.is_modified:
sub = row.row(align=True)
sub.alert = True
sub.operator("text.resolve_conflict", text="", icon='HELP')
row = layout.row(align=True)
row.template_ID(st, "text", new="text.new", unlink="text.unlink", open="text.open")
row = layout.row(align=True)
row.prop(st, "show_line_numbers", text="")
row.prop(st, "show_word_wrap", text="")
row.prop(st, "show_syntax_highlight", text="")
if text:
osl = text.name.endswith(".osl") or text.name.endswith(".oso")
if osl:
row = layout.row()
row.operator("node.shader_script_update")
else:
row = layout.row()
row.operator("text.run_script")
row = layout.row()
row.active = text.name.endswith(".py")
row.prop(text, "use_module")
row = layout.row()
if text.filepath:
if text.is_dirty:
row.label(text=iface_("File: *%r (unsaved)") %
text.filepath, translate=False)
else:
row.label(text=iface_("File: %r") %
text.filepath, translate=False)
else:
row.label(text="Text: External"
if text.library
else "Text: Internal")
class TEXT_MT_editor_menus(Menu):
bl_idname = "TEXT_MT_editor_menus"
bl_label = ""
def draw(self, context):
self.draw_menus(self.layout, context)
@staticmethod
def draw_menus(layout, context):
st = context.space_data
text = st.text
layout.menu("TEXT_MT_view")
layout.menu("TEXT_MT_text")
if text:
layout.menu("TEXT_MT_edit")
layout.menu("TEXT_MT_format")
layout.menu("TEXT_MT_templates")
class TEXT_PT_properties(Panel):
bl_space_type = 'TEXT_EDITOR'
bl_region_type = 'UI'
bl_label = "Properties"
def draw(self, context):
layout = self.layout
st = context.space_data
flow = layout.column_flow()
flow.prop(st, "show_line_numbers")
flow.prop(st, "show_word_wrap")
flow.prop(st, "show_syntax_highlight")
flow.prop(st, "show_line_highlight")
flow.prop(st, "use_live_edit")
flow = layout.column_flow()
flow.prop(st, "font_size")
flow.prop(st, "tab_width")
text = st.text
if text:
flow.prop(text, "use_tabs_as_spaces")
flow.prop(st, "show_margin")
col = flow.column()
col.active = st.show_margin
col.prop(st, "margin_column")
class TEXT_PT_find(Panel):
bl_space_type = 'TEXT_EDITOR'
bl_region_type = 'UI'
bl_label = "Find"
def draw(self, context):
layout = self.layout
st = context.space_data
# find
col = layout.column(align=True)
row = col.row(align=True)
row.prop(st, "find_text", text="")
row.operator("text.find_set_selected", text="", icon='TEXT')
col.operator("text.find")
# replace
col = layout.column(align=True)
row = col.row(align=True)
row.prop(st, "replace_text", text="")
row.operator("text.replace_set_selected", text="", icon='TEXT')
col.operator("text.replace")
# settings
layout.prop(st, "use_match_case")
row = layout.row(align=True)
row.prop(st, "use_find_wrap", text="Wrap")
row.prop(st, "use_find_all", text="All")
class TEXT_MT_view(Menu):
bl_label = "View"
def draw(self, context):
layout = self.layout
layout.operator("text.properties", icon='MENU_PANEL')
layout.separator()
layout.operator("text.move",
text="Top of File",
).type = 'FILE_TOP'
layout.operator("text.move",
text="Bottom of File",
).type = 'FILE_BOTTOM'
layout.separator()
layout.operator("screen.area_dupli")
layout.operator("screen.screen_full_area")
layout.operator("screen.screen_full_area", text="Toggle Fullscreen Area").use_hide_panels = True
class TEXT_MT_text(Menu):
bl_label = "Text"
def draw(self, context):
layout = self.layout
st = context.space_data
text = st.text
layout.operator("text.new")
layout.operator("text.open")
if text:
layout.operator("text.reload")
layout.column()
layout.operator("text.save")
layout.operator("text.save_as")
if text.filepath:
layout.operator("text.make_internal")
layout.column()
layout.operator("text.run_script")
class TEXT_MT_templates_py(Menu):
bl_label = "Python"
def draw(self, context):
self.path_menu(
bpy.utils.script_paths("templates_py"),
"text.open",
props_default={"internal": True},
)
class TEXT_MT_templates_osl(Menu):
bl_label = "Open Shading Language"
def draw(self, context):
self.path_menu(
bpy.utils.script_paths("templates_osl"),
"text.open",
props_default={"internal": True},
)
class TEXT_MT_templates_pov(Menu):
bl_label = "POV-Ray Scene Description Language"
def draw(self, context):
self.path_menu(
bpy.utils.script_paths("templates_pov"),
"text.open",
props_default={"internal": True},
)
class TEXT_MT_templates(Menu):
bl_label = "Templates"
def draw(self, context):
layout = self.layout
layout.menu("TEXT_MT_templates_py")
layout.menu("TEXT_MT_templates_osl")
layout.menu("TEXT_MT_templates_pov")
class TEXT_MT_edit_select(Menu):
bl_label = "Select"
def draw(self, context):
layout = self.layout
layout.operator("text.select_all")
layout.operator("text.select_line")
class TEXT_MT_format(Menu):
bl_label = "Format"
def draw(self, context):
layout = self.layout
layout.operator("text.indent")
layout.operator("text.unindent")
layout.separator()
layout.operator("text.comment")
layout.operator("text.uncomment")
layout.separator()
layout.operator_menu_enum("text.convert_whitespace", "type")
class TEXT_MT_edit_to3d(Menu):
bl_label = "Text To 3D Object"
def draw(self, context):
layout = self.layout
layout.operator("text.to_3d_object",
text="One Object",
).split_lines = False
layout.operator("text.to_3d_object",
text="One Object Per Line",
).split_lines = True
class TEXT_MT_edit(Menu):
bl_label = "Edit"
@classmethod
def poll(cls, context):
return (context.space_data.text)
def draw(self, context):
layout = self.layout
layout.operator("ed.undo")
layout.operator("ed.redo")
layout.separator()
layout.operator("text.cut")
layout.operator("text.copy")
layout.operator("text.paste")
layout.operator("text.duplicate_line")
layout.separator()
layout.operator("text.move_lines",
text="Move line(s) up").direction = 'UP'
layout.operator("text.move_lines",
text="Move line(s) down").direction = 'DOWN'
layout.separator()
layout.menu("TEXT_MT_edit_select")
layout.separator()
layout.operator("text.jump")
layout.operator("text.start_find", text="Find...")
layout.operator("text.autocomplete")
layout.separator()
layout.menu("TEXT_MT_edit_to3d")
class TEXT_MT_toolbox(Menu):
bl_label = ""
def draw(self, context):
layout = self.layout
layout.operator_context = 'INVOKE_DEFAULT'
layout.operator("text.cut")
layout.operator("text.copy")
layout.operator("text.paste")
layout.separator()
layout.operator("text.run_script")
classes = (
TEXT_HT_header,
TEXT_MT_edit,
TEXT_MT_editor_menus,
TEXT_PT_properties,
TEXT_PT_find,
TEXT_MT_view,
TEXT_MT_text,
TEXT_MT_templates,
TEXT_MT_templates_py,
TEXT_MT_templates_osl,
TEXT_MT_templates_pov,
TEXT_MT_edit_select,
TEXT_MT_format,
TEXT_MT_edit_to3d,
TEXT_MT_toolbox,
)
if __name__ == "__main__": # only for live edit.
from bpy.utils import register_class
for cls in classes:
register_class(cls)
Notice that the special command line
# <pep8-80 compliant>
writes pep-80 rather than pep-8 as getting closer to the native blender program often requires more demanding standards : pep-80 asks for even shorter line lengths and so a file passing the pep-80 automated tests that the Blender Foundation regularly runs might fail passing PEP-80.