[Blender Export][Added] Options useful to generate simple animations
- The resulting PNGs can be converted to MP4 using ffmpeg
This commit is contained in:
parent
4202f01c01
commit
948a40fb91
|
|
@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
(#470)
|
||||
- Blender export:
|
||||
- Support for pcb2blender v2.6 (Blender 3.5.1)
|
||||
- `auto_camera_z_axis_factor`: used to control the default camera distance
|
||||
- Options to create simple animations:
|
||||
- PoV `steps`: to create rotation angle increments
|
||||
- `default_file_id`: can be used to create numbered PNGs
|
||||
- `fixed_auto_camera`: to avoid adjusting the automatic camera on each frame
|
||||
- Populate:
|
||||
- Basic support for regular list items (#480)
|
||||
|
||||
|
|
@ -26,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Problems when trying to aggregate the datasheet field (#472)
|
||||
- kibot-check:
|
||||
- Show 7.x as supported (#469)
|
||||
- Blender export:
|
||||
- Rotations are now applied to the current view, not just the top view
|
||||
|
||||
|
||||
## [1.6.3] - 2023-06-26
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -1736,11 +1736,15 @@ Notes:
|
|||
* Valid keys:
|
||||
- **`view`**: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
Compatible with `render_3d`.
|
||||
- `file_id`: [string=''] String to diferentiate the name of this view.
|
||||
When empty we use the `view`.
|
||||
- `file_id`: [string=''] String to diferentiate the name of this point of view.
|
||||
When empty we use the `default_file_id` or the `view`.
|
||||
- `rotate_x`: [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees].
|
||||
- `rotate_y`: [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees].
|
||||
- `rotate_z`: [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees].
|
||||
- `steps`: [number=1] [1-1000] Generate this amount of steps using the rotation angles as increments.
|
||||
Use a value of 1 (default) to interpret the angles as absolute.
|
||||
Used for animations. You should define the `default_file_id` to something like
|
||||
'_%03d' to get the animation frames.
|
||||
- **`render_options`**: [dict] Controls how the render is done for the `render` output type.
|
||||
* Valid keys:
|
||||
- **`samples`**: [number=10] How many samples we create. Each sample is a raytracing render.
|
||||
|
|
@ -1756,6 +1760,8 @@ Notes:
|
|||
- *width*: Alias for resolution_x.
|
||||
- `add_default_light`: [boolean=true] Add a default light when none specified.
|
||||
The default light is located at (-size*3.33, size*3.33, size*5) where size is max(width, height) of the PCB.
|
||||
- `auto_camera_z_axis_factor`: [number=1.1] Value to multiply the Z axis coordinate after computing the automatically generated camera.
|
||||
Used to avoid collision of the camera and the object.
|
||||
- `camera`: [dict] Options for the camera.
|
||||
If none specified KiBot will create a suitable camera.
|
||||
If no position is specified for the camera KiBot will look for a suitable position.
|
||||
|
|
@ -1765,6 +1771,10 @@ Notes:
|
|||
- `pos_y`: [number|string] Y position [m]. You can use `width`, `height` and `size` for PCB dimensions.
|
||||
- `pos_z`: [number|string] Z position [m]. You can use `width`, `height` and `size` for PCB dimensions.
|
||||
- `type`: [string='perspective'] [perspective,orthographic,panoramic] Type of camera.
|
||||
- `default_file_id`: [string=''] Default value for the `file_id` in the `point_of_view` options.
|
||||
Use something like '_%03d' for animations.
|
||||
- `fixed_auto_camera`: [boolean=false] When using the automatically generated camera and multiple points of view this option computes the camera
|
||||
position just once. Suitable for videos.
|
||||
- `light`: [dict|list(dict)] Options for the light/s.
|
||||
* Valid keys:
|
||||
- `energy`: [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful.
|
||||
|
|
@ -1959,8 +1969,8 @@ Notes:
|
|||
- `logo_scale`: [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software.
|
||||
- `max_col_width`: [number=60] [20,999] Maximum column width (characters).
|
||||
- `mouser_link`: [string|list(string)=''] Column/s containing Mouser part numbers, will be linked to web page.
|
||||
- `specs_columns`: [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references,
|
||||
'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
|
||||
- `specs_columns`: [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the
|
||||
references, 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
|
||||
Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
|
||||
'_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'.
|
||||
* Valid keys:
|
||||
|
|
@ -3370,8 +3380,8 @@ Notes:
|
|||
Plugin: Uses an external python function, only `code` and `arg` are relevant.
|
||||
- `arg`: [string=''] Argument to pass to the plugin. Used for *plugin*.
|
||||
- `code`: [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin*.
|
||||
- `cutout`: [number|string] When your design features open pockets on the side, this parameter specifies extra cutout depth in order to
|
||||
ensure that a sharp corner of the pocket can be milled. Used for *full*.
|
||||
- `cutout`: [number|string] When your design features open pockets on the side, this parameter specifies extra cutout
|
||||
depth in order to ensure that a sharp corner of the pocket can be milled. Used for *full*.
|
||||
- `hcount`: [number=1] Number of tabs in the horizontal direction. Used for *fixed*.
|
||||
- `hwidth`: [number|string] The width of tabs in the horizontal direction. Used for *fixed* and *spacing*.
|
||||
- *min_distance*: Alias for mindistance.
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ outputs:
|
|||
# [boolean=true] Add a default light when none specified.
|
||||
# The default light is located at (-size*3.33, size*3.33, size*5) where size is max(width, height) of the PCB
|
||||
add_default_light: true
|
||||
# [number=1.1] Value to multiply the Z axis coordinate after computing the automatically generated camera.
|
||||
# Used to avoid collision of the camera and the object
|
||||
auto_camera_z_axis_factor: 1.1
|
||||
# [dict] Options for the camera.
|
||||
# If none specified KiBot will create a suitable camera.
|
||||
# If no position is specified for the camera KiBot will look for a suitable position
|
||||
|
|
@ -124,6 +127,12 @@ outputs:
|
|||
pos_z: 0
|
||||
# [string='perspective'] [perspective,orthographic,panoramic] Type of camera
|
||||
type: 'perspective'
|
||||
# [string=''] Default value for the `file_id` in the `point_of_view` options.
|
||||
# Use something like '_%03d' for animations
|
||||
default_file_id: ''
|
||||
# [boolean=false] When using the automatically generated camera and multiple points of view this option computes the camera
|
||||
# position just once. Suitable for videos
|
||||
fixed_auto_camera: false
|
||||
# [dict|list(dict)] Options for the light/s
|
||||
light:
|
||||
# [number=0] How powerful is the light. Using 0 for POINT and SUN KiBot will try to use something useful
|
||||
|
|
@ -214,8 +223,8 @@ outputs:
|
|||
texture_dpi: 1016.0
|
||||
# [dict|list(dict)] How the object is viewed by the camera
|
||||
point_of_view:
|
||||
# [string=''] String to diferentiate the name of this view.
|
||||
# When empty we use the `view`
|
||||
# [string=''] String to diferentiate the name of this point of view.
|
||||
# When empty we use the `default_file_id` or the `view`
|
||||
- file_id: ''
|
||||
# [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]
|
||||
rotate_x: 0
|
||||
|
|
@ -223,6 +232,11 @@ outputs:
|
|||
rotate_y: 0
|
||||
# [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]
|
||||
rotate_z: 0
|
||||
# [number=1] [1-1000] Generate this amount of steps using the rotation angles as increments.
|
||||
# Use a value of 1 (default) to interpret the angles as absolute.
|
||||
# Used for animations. You should define the `default_file_id` to something like
|
||||
# '_%03d' to get the animation frames
|
||||
steps: 1
|
||||
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
# Compatible with `render_3d`
|
||||
view: 'top'
|
||||
|
|
@ -556,8 +570,8 @@ outputs:
|
|||
# [boolean=false] Enable Specs worksheet creation. Contains specifications for the components.
|
||||
# Works with only some KiCost APIs
|
||||
specs: false
|
||||
# [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references,
|
||||
# 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
|
||||
# [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the
|
||||
# references, 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
|
||||
# Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
|
||||
# '_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'
|
||||
specs_columns:
|
||||
|
|
@ -1882,8 +1896,8 @@ outputs:
|
|||
arg: ''
|
||||
# [string=''] Plugin specification (PACKAGE.FUNCTION or PYTHON_FILE.FUNCTION). Used for *plugin*
|
||||
code: ''
|
||||
# [number|string] When your design features open pockets on the side, this parameter specifies extra cutout depth in order to
|
||||
# ensure that a sharp corner of the pocket can be milled. Used for *full*
|
||||
# [number|string] When your design features open pockets on the side, this parameter specifies extra cutout
|
||||
# depth in order to ensure that a sharp corner of the pocket can be milled. Used for *full*
|
||||
cutout: 1
|
||||
# [number=1] Number of tabs in the horizontal direction. Used for *fixed*
|
||||
hcount: 1
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ import sys # to get command line args
|
|||
import addon_utils
|
||||
import bpy
|
||||
|
||||
X_AXIS = 0
|
||||
Y_AXIS = 1
|
||||
Z_AXIS = 2
|
||||
# X_AXIS = 0
|
||||
# Y_AXIS = 1
|
||||
# Z_AXIS = 2
|
||||
X_AXIS = 'X'
|
||||
Y_AXIS = 'Y'
|
||||
Z_AXIS = 'Z'
|
||||
VALID_FORMATS = {'fbx': 'Filmbox, proprietary format developed by Kaydara (owned by Autodesk)',
|
||||
'obj': 'geometry definition format developed by Wavefront Technologies. Currently open',
|
||||
'x3d': 'VRML successor. A royalty-free ISO/IEC standard for declaratively representing 3D graphics.',
|
||||
|
|
@ -70,38 +73,26 @@ def render_export(render_path):
|
|||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
def rot(axis, deg):
|
||||
bpy.context.active_object.rotation_euler[axis] = math.radians(deg)
|
||||
|
||||
|
||||
def do_rotate(scene, name, id):
|
||||
rotate = scene.get(name)
|
||||
if rotate:
|
||||
rot(id, rotate)
|
||||
def do_rotate(rots):
|
||||
bpy.ops.transform.rotate(value=math.radians(-rots[0]), orient_axis='X', center_override=(0, 0, 0))
|
||||
bpy.ops.transform.rotate(value=math.radians(-rots[1]), orient_axis='Y', center_override=(0, 0, 0))
|
||||
bpy.ops.transform.rotate(value=math.radians(-rots[2]), orient_axis='Z', center_override=(0, 0, 0))
|
||||
|
||||
|
||||
def do_point_of_view(scene, name):
|
||||
view = scene.get(name)
|
||||
if view is None or view == 'z':
|
||||
return
|
||||
return (0, 0, 0)
|
||||
if view == 'Z':
|
||||
rot(Y_AXIS, 180)
|
||||
return
|
||||
return (0, 180, 0)
|
||||
if view == 'y':
|
||||
rot(X_AXIS, -90)
|
||||
return
|
||||
return (-90, 0, 0)
|
||||
if view == 'Y':
|
||||
rot(X_AXIS, 90)
|
||||
rot(Z_AXIS, 180)
|
||||
return
|
||||
return (90, 0, 180)
|
||||
if view == 'x':
|
||||
rot(Y_AXIS, 90)
|
||||
rot(Z_AXIS, 90)
|
||||
return
|
||||
return (0, 90, 90)
|
||||
if view == 'X':
|
||||
rot(Y_AXIS, -90)
|
||||
rot(Z_AXIS, -90)
|
||||
return
|
||||
return (0, -90, -90)
|
||||
|
||||
|
||||
def srgb_to_linearrgb(c):
|
||||
|
|
@ -191,18 +182,19 @@ def create_background_gradient(color1, color0):
|
|||
jscene = None
|
||||
auto_camera = False
|
||||
cam_ob = None
|
||||
cur_rot = None
|
||||
location = None
|
||||
|
||||
|
||||
def apply_scene(file, n_view=0):
|
||||
def apply_start_scene(file):
|
||||
# Loads scene
|
||||
if not file:
|
||||
return 1
|
||||
global jscene
|
||||
if jscene is None:
|
||||
with open(file, 'rt') as f:
|
||||
text = f.read()
|
||||
print(text)
|
||||
jscene = json.loads(text)
|
||||
with open(file, 'rt') as f:
|
||||
text = f.read()
|
||||
print(text)
|
||||
jscene = json.loads(text)
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Select the board
|
||||
|
|
@ -210,72 +202,77 @@ def apply_scene(file, n_view=0):
|
|||
bpy.ops.object.select_all(action='SELECT')
|
||||
# Make sure we start rotations from 0
|
||||
bpy.context.active_object.rotation_euler = (0, 0, 0)
|
||||
global location
|
||||
location = bpy.context.active_object.location.copy()
|
||||
povs = jscene.get('point_of_view')
|
||||
if povs:
|
||||
pov = povs[n_view]
|
||||
global cur_rot
|
||||
pov = povs[0]
|
||||
# Apply point of view
|
||||
do_point_of_view(pov, 'view')
|
||||
cur_rot = do_point_of_view(pov, 'view')
|
||||
# Apply extra rotations
|
||||
do_rotate(pov, 'rotate_x', 0)
|
||||
do_rotate(pov, 'rotate_y', 1)
|
||||
do_rotate(pov, 'rotate_z', 2)
|
||||
cur_rot = (cur_rot[0] + pov.get('rotate_x', 0),
|
||||
cur_rot[1] + pov.get('rotate_y', 0),
|
||||
cur_rot[2] + pov.get('rotate_z', 0))
|
||||
print(f'- Initial rotations: {cur_rot}')
|
||||
do_rotate(cur_rot)
|
||||
|
||||
# Add a camera
|
||||
global auto_camera
|
||||
if not n_view:
|
||||
# First time: create the camera
|
||||
camera = jscene.get('camera')
|
||||
if not camera:
|
||||
# First time: create the camera
|
||||
camera = jscene.get('camera')
|
||||
if not camera:
|
||||
auto_camera = True
|
||||
name = 'kibot_camera'
|
||||
pos = (0.0, 0.0, 10.0)
|
||||
type = 'PERSP'
|
||||
else:
|
||||
name = camera.get('name', 'unknown')
|
||||
pos = camera.get('position', None)
|
||||
type = camera.get('type', 'PERSP')
|
||||
if pos is None:
|
||||
auto_camera = True
|
||||
name = 'kibot_camera'
|
||||
pos = (0.0, 0.0, 10.0)
|
||||
type = 'PERSP'
|
||||
pos = (0, 0, 0)
|
||||
else:
|
||||
name = camera.get('name', 'unknown')
|
||||
pos = camera.get('position', None)
|
||||
type = camera.get('type', 'PERSP')
|
||||
if pos is None:
|
||||
auto_camera = True
|
||||
pos = (0, 0, 0)
|
||||
else:
|
||||
auto_camera = False
|
||||
print(f"- Creating camera {name} at {pos}")
|
||||
cam_data = bpy.data.cameras.new(name)
|
||||
global cam_ob
|
||||
cam_ob = bpy.data.objects.new(name=name, object_data=cam_data)
|
||||
scene.collection.objects.link(cam_ob) # instance the camera object in the scene
|
||||
scene.camera = cam_ob # set the active camera
|
||||
cam_ob.location = pos
|
||||
cam_ob.data.type = type
|
||||
auto_camera = False
|
||||
print(f"- Creating camera {name} at {pos}")
|
||||
cam_data = bpy.data.cameras.new(name)
|
||||
global cam_ob
|
||||
cam_ob = bpy.data.objects.new(name=name, object_data=cam_data)
|
||||
scene.collection.objects.link(cam_ob) # instance the camera object in the scene
|
||||
scene.camera = cam_ob # set the active camera
|
||||
cam_ob.location = pos
|
||||
cam_ob.data.type = type
|
||||
|
||||
if auto_camera:
|
||||
print('- Changing camera to focus the board')
|
||||
bpy.ops.view3d.camera_to_view_selected()
|
||||
cam_ob.location = (cam_ob.location[0], cam_ob.location[1], cam_ob.location[2]*1.1)
|
||||
cam_ob.location = (cam_ob.location[0], cam_ob.location[1],
|
||||
cam_ob.location[2]*jscene.get('auto_camera_z_axis_factor', 1.1))
|
||||
|
||||
# Add lights
|
||||
if not n_view:
|
||||
# First time: create the lights
|
||||
lights = jscene.get('lights')
|
||||
if lights:
|
||||
for light in lights:
|
||||
name = light.get('name', 'unknown')
|
||||
pos = light.get('position', (0.0, 0.0, 0.0))
|
||||
typ = light.get('type', 'SUN')
|
||||
energy = light.get('energy', 0.0)
|
||||
print(f"- Creating light {name} at {pos}, type: {typ} energy: {energy}")
|
||||
light_data = bpy.data.lights.new(name, typ)
|
||||
print(f"- Default energy: {light_data.energy}")
|
||||
if energy:
|
||||
light_data.energy = energy
|
||||
light_ob = bpy.data.objects.new(name=name, object_data=light_data)
|
||||
scene.collection.objects.link(light_ob)
|
||||
light_ob.location = pos
|
||||
# First time: create the lights
|
||||
lights = jscene.get('lights')
|
||||
if lights:
|
||||
for light in lights:
|
||||
name = light.get('name', 'unknown')
|
||||
pos = light.get('position', (0.0, 0.0, 0.0))
|
||||
typ = light.get('type', 'SUN')
|
||||
energy = light.get('energy', 0.0)
|
||||
print(f"- Creating light {name} at {pos}, type: {typ} energy: {energy}")
|
||||
light_data = bpy.data.lights.new(name, typ)
|
||||
print(f"- Default energy: {light_data.energy}")
|
||||
if energy:
|
||||
light_data.energy = energy
|
||||
light_ob = bpy.data.objects.new(name=name, object_data=light_data)
|
||||
scene.collection.objects.link(light_ob)
|
||||
light_ob.location = pos
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
# Setup render options
|
||||
render = jscene.get('render')
|
||||
if render and not n_view:
|
||||
if render:
|
||||
scene.cycles.samples = render.get('samples', 10)
|
||||
r = scene.render
|
||||
r.engine = 'CYCLES'
|
||||
|
|
@ -291,6 +288,43 @@ def apply_scene(file, n_view=0):
|
|||
return len(povs)
|
||||
|
||||
|
||||
def apply_scene(n_view):
|
||||
global jscene
|
||||
if jscene is None:
|
||||
return
|
||||
|
||||
# Make sure we start rotations from 0
|
||||
povs = jscene.get('point_of_view')
|
||||
if povs:
|
||||
global cur_rot
|
||||
pov = povs[n_view]
|
||||
# Apply point of view
|
||||
new_rot = do_point_of_view(pov, 'view')
|
||||
# Apply extra rotations
|
||||
new_rot = (new_rot[0] + pov.get('rotate_x', 0),
|
||||
new_rot[1] + pov.get('rotate_y', 0),
|
||||
new_rot[2] + pov.get('rotate_z', 0))
|
||||
if new_rot != cur_rot:
|
||||
print(f'- Rotations: {new_rot}')
|
||||
# Reset the position
|
||||
bpy.context.active_object.location = location.copy()
|
||||
# Reset the rotation
|
||||
bpy.context.active_object.rotation_euler = (0, 0, 0)
|
||||
# Apply the new rotation
|
||||
do_rotate(new_rot)
|
||||
cur_rot = new_rot
|
||||
|
||||
# Move the camera
|
||||
global auto_camera
|
||||
if auto_camera and not jscene.get('fixed_auto_camera', False):
|
||||
print('- Changing camera to focus the board')
|
||||
bpy.ops.view3d.camera_to_view_selected()
|
||||
cam_ob.location = (cam_ob.location[0], cam_ob.location[1],
|
||||
cam_ob.location[2]*jscene.get('auto_camera_z_axis_factor', 1.1))
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
|
||||
EXPORTERS = {'fbx': fbx_export,
|
||||
'obj': obj_export,
|
||||
'x3d': x3d_export,
|
||||
|
|
@ -369,7 +403,7 @@ def main():
|
|||
ops["center_boards"] = args.dont_center
|
||||
bpy.ops.pcb2blender.import_pcb3d(**ops)
|
||||
# Apply the scene first scene
|
||||
c_views = apply_scene(args.scene)
|
||||
c_views = apply_start_scene(args.scene)
|
||||
c_formats = len(args.format)
|
||||
if c_formats % c_views:
|
||||
print("The number of outputs must be a multiple of the views (views: {} outputs: {})".format(c_views, c_formats))
|
||||
|
|
@ -378,7 +412,7 @@ def main():
|
|||
for n in range(c_views):
|
||||
if n:
|
||||
# Apply scene N
|
||||
apply_scene(args.scene, n)
|
||||
apply_scene(n)
|
||||
# Get the current slice
|
||||
formats = args.format[n*per_pass:(n+1)*per_pass]
|
||||
outputs = args.output[n*per_pass:(n+1)*per_pass]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Dependencies:
|
|||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from .error import KiPlotConfigurationError
|
||||
from .kiplot import get_output_targets, run_output, run_command, register_xmp_import, config_output, configure_and_run
|
||||
|
|
@ -26,6 +27,7 @@ from . import log
|
|||
|
||||
logger = log.get_logger()
|
||||
bb = None
|
||||
RE_FILE_ID = re.compile(r"\%\d*d")
|
||||
|
||||
|
||||
def get_board_size():
|
||||
|
|
@ -207,10 +209,14 @@ class BlenderPointOfViewOptions(Optionable):
|
|||
""" *[top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
|
||||
Compatible with `render_3d` """
|
||||
self.file_id = ''
|
||||
""" String to diferentiate the name of this view.
|
||||
When empty we use the `view` """
|
||||
""" String to diferentiate the name of this point of view.
|
||||
When empty we use the `default_file_id` or the `view` """
|
||||
self.steps = 1
|
||||
""" [1-1000] Generate this amount of steps using the rotation angles as increments.
|
||||
Use a value of 1 (default) to interpret the angles as absolute.
|
||||
Used for animations. You should define the `default_file_id` to something like
|
||||
'_%03d' to get the animation frames """
|
||||
self._unknown_is_error = True
|
||||
self._file_id = ''
|
||||
|
||||
def config(self, parent):
|
||||
super().config(parent)
|
||||
|
|
@ -218,9 +224,14 @@ class BlenderPointOfViewOptions(Optionable):
|
|||
view = self._views.get(self.view, None)
|
||||
if view is not None:
|
||||
self.view = view
|
||||
self._file_id = self.file_id
|
||||
if not self._file_id:
|
||||
self._file_id = '_'+self._rviews.get(self.view)
|
||||
|
||||
def get_view(self):
|
||||
return self._rviews.get(self.view, 'no_view')
|
||||
|
||||
def increment(self, inc):
|
||||
self.rotate_x += inc.rotate_x
|
||||
self.rotate_y += inc.rotate_y
|
||||
self.rotate_z += inc.rotate_z
|
||||
|
||||
|
||||
class PCB3DExportOptions(Base3DOptionsWithHL):
|
||||
|
|
@ -304,6 +315,15 @@ class Blender_ExportOptions(BaseOptions):
|
|||
""" [dict] Options for the camera.
|
||||
If none specified KiBot will create a suitable camera.
|
||||
If no position is specified for the camera KiBot will look for a suitable position """
|
||||
self.fixed_auto_camera = False
|
||||
""" When using the automatically generated camera and multiple points of view this option computes the camera
|
||||
position just once. Suitable for videos """
|
||||
self.auto_camera_z_axis_factor = 1.1
|
||||
""" Value to multiply the Z axis coordinate after computing the automatically generated camera.
|
||||
Used to avoid collision of the camera and the object """
|
||||
self.default_file_id = ''
|
||||
""" Default value for the `file_id` in the `point_of_view` options.
|
||||
Use something like '_%03d' for animations """
|
||||
self.render_options = BlenderRenderOptions
|
||||
""" *[dict] Controls how the render is done for the `render` output type """
|
||||
self.point_of_view = BlenderPointOfViewOptions
|
||||
|
|
@ -369,7 +389,7 @@ class Blender_ExportOptions(BaseOptions):
|
|||
elif isinstance(self.point_of_view, BlenderPointOfViewOptions):
|
||||
self.point_of_view = [self.point_of_view]
|
||||
|
||||
def get_output_filename(self, o, output_dir, pov):
|
||||
def get_output_filename(self, o, output_dir, pov, order):
|
||||
if o.type == 'render':
|
||||
self._expand_ext = 'png'
|
||||
elif o.type == 'blender':
|
||||
|
|
@ -377,7 +397,15 @@ class Blender_ExportOptions(BaseOptions):
|
|||
else:
|
||||
self._expand_ext = o.type
|
||||
cur_id = self._expand_id
|
||||
self._expand_id += pov._file_id
|
||||
file_id = pov.file_id
|
||||
if not file_id:
|
||||
file_id = self.default_file_id or ('_'+pov.get_view())
|
||||
m = RE_FILE_ID.search(file_id)
|
||||
if m:
|
||||
res = m.group(0)
|
||||
val = res % order
|
||||
file_id = file_id.replace(res, val)
|
||||
self._expand_id += file_id
|
||||
name = self._parent.expand_filename(output_dir, o.output)
|
||||
self._expand_id = cur_id
|
||||
return name
|
||||
|
|
@ -530,6 +558,8 @@ class Blender_ExportOptions(BaseOptions):
|
|||
if (hasattr(ca, '_pos_x_user_defined') or hasattr(ca, '_pos_y_user_defined') or
|
||||
hasattr(ca, '_pos_z_user_defined')):
|
||||
scene['camera']['position'] = (ca.pos_x, ca.pos_y, ca.pos_z)
|
||||
scene['fixed_auto_camera'] = self.fixed_auto_camera
|
||||
scene['auto_camera_z_axis_factor'] = self.auto_camera_z_axis_factor
|
||||
ro = self.render_options
|
||||
scene['render'] = {'samples': ro.samples,
|
||||
'resolution_x': ro.resolution_x,
|
||||
|
|
@ -538,11 +568,21 @@ class Blender_ExportOptions(BaseOptions):
|
|||
'background1': ro.background1,
|
||||
'background2': ro.background2}
|
||||
povs = []
|
||||
last_pov = BlenderPointOfViewOptions()
|
||||
for pov in self.point_of_view:
|
||||
povs.append({'rotate_x': -pov.rotate_x,
|
||||
'rotate_y': -pov.rotate_y,
|
||||
'rotate_z': -pov.rotate_z,
|
||||
'view': pov.view})
|
||||
if pov.steps > 1:
|
||||
for _ in range(pov.steps):
|
||||
last_pov.increment(pov)
|
||||
povs.append({'rotate_x': -last_pov.rotate_x,
|
||||
'rotate_y': -last_pov.rotate_y,
|
||||
'rotate_z': -last_pov.rotate_z,
|
||||
'view': pov.view})
|
||||
else:
|
||||
povs.append({'rotate_x': -pov.rotate_x,
|
||||
'rotate_y': -pov.rotate_y,
|
||||
'rotate_z': -pov.rotate_z,
|
||||
'view': pov.view})
|
||||
last_pov = pov
|
||||
scene['point_of_view'] = povs
|
||||
text = json.dumps(scene, sort_keys=True, indent=2)
|
||||
logger.debug('Scene:\n'+text)
|
||||
|
|
@ -569,18 +609,22 @@ class Blender_ExportOptions(BaseOptions):
|
|||
if not pi.stack_boards:
|
||||
cmd.append('--dont_stack_boards')
|
||||
cmd.append('--format')
|
||||
for _ in self.point_of_view:
|
||||
for o in self.outputs:
|
||||
cmd.append(o.type)
|
||||
for pov in self.point_of_view:
|
||||
for _ in range(pov.steps):
|
||||
for o in self.outputs:
|
||||
cmd.append(o.type)
|
||||
cmd.append('--output')
|
||||
names = set()
|
||||
order = 1
|
||||
for pov in self.point_of_view:
|
||||
for o in self.outputs:
|
||||
name = self.get_output_filename(o, self._parent.output_dir, pov)
|
||||
if name in names:
|
||||
raise KiPlotConfigurationError('Repeated name (use `file_id`): '+name)
|
||||
cmd.append(name)
|
||||
names.add(name)
|
||||
for _ in range(pov.steps):
|
||||
for o in self.outputs:
|
||||
name = self.get_output_filename(o, self._parent.output_dir, pov, order)
|
||||
if name in names:
|
||||
raise KiPlotConfigurationError('Repeated name (use `file_id`): '+name)
|
||||
cmd.append(name)
|
||||
names.add(name)
|
||||
order += 1
|
||||
cmd.extend(['--scene', f.name])
|
||||
cmd.append(pcb3d_file)
|
||||
# Execute the command
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# KiBot Blender export test multiple view points
|
||||
# Multiple points of view
|
||||
kibot:
|
||||
version: 1
|
||||
|
||||
outputs:
|
||||
- name: '3d_video_export'
|
||||
comment: "Generates the frames for a video of a rotating PCB"
|
||||
type: blender_export
|
||||
options:
|
||||
render_options:
|
||||
transparent_background: true
|
||||
samples: 10
|
||||
fixed_auto_camera: true
|
||||
auto_camera_z_axis_factor: 1.5
|
||||
default_file_id: '_%03d'
|
||||
point_of_view:
|
||||
- rotate_x: 30
|
||||
rotate_z: -20
|
||||
- rotate_x: 4
|
||||
rotate_z: 4
|
||||
steps: 90
|
||||
outputs:
|
||||
- type: render
|
||||
Loading…
Reference in New Issue