[Blender Export] Multiple Point of Views

- Moved the POV options to a separated section
- Allowed to have more than one POV
This commit is contained in:
Salvador E. Tropea 2023-01-24 20:29:08 -03:00
parent f860b7eb11
commit a7063ea937
7 changed files with 194 additions and 122 deletions

View File

@ -1590,24 +1590,24 @@ Notes:
A short-cut to use for simple cases where a variant is an overkill.
- `variant`: [string=''] Board variant to apply.
- `version`: [string='2.1'] [2.1,2.1_haschtl] Variant of the format used.
- **`render_options`**: [dict] Render and point of view options.
Controls how the render is done for the `render` output type,
and how the object is viewed by the camera.
- **`point_of_view`**: [dict|list(dict)] How the object is viewed by the camera.
* 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`.
- `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].
- **`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.
Use 1 for a raw preview, 10 for a draft and 100 or more for the final render.
- **`transparent_background`**: [boolean=false] Make the background transparent.
- **`view`**: [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
Compatible with `render_3d`.
- `background1`: [string='#66667F'] First color for the background gradient.
- `background2`: [string='#CCCCE5'] Second color for the background gradient.
- `file_id`: [string=''] String to diferentiate the name of this view.
When empty we use the `view`.
- `resolution_x`: [number=1280] Width of the image.
- `resolution_y`: [number=720] Height of the image.
- `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].
- `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.
- `camera`: [dict] Options for the camera.

View File

@ -193,35 +193,35 @@ outputs:
stack_boards: true
# [number=1016.0] [508-2032] Texture density in dots per inch
texture_dpi: 1016.0
# [dict] Render and point of view options.
# Controls how the render is done for the `render` output type,
# and how the object is viewed by the camera
# [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`
- file_id: ''
# [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]
rotate_x: 0
# [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees]
rotate_y: 0
# [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]
rotate_z: 0
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
# Compatible with `render_3d`
view: 'top'
# [dict] Controls how the render is done for the `render` output type
render_options:
# [string='#66667F'] First color for the background gradient
background1: '#66667F'
# [string='#CCCCE5'] Second color for the background gradient
background2: '#CCCCE5'
# [string=''] String to diferentiate the name of this view.
# When empty we use the `view`
file_id: ''
# [number=1280] Width of the image
resolution_x: 1280
# [number=720] Height of the image
resolution_y: 720
# [number=0] Angle to rotate the board in the X axis, positive is clockwise [degrees]
rotate_x: 0
# [number=0] Angle to rotate the board in the Y axis, positive is clockwise [degrees]
rotate_y: 0
# [number=0] Angle to rotate the board in the Z axis, positive is clockwise [degrees]
rotate_z: 0
# [number=10] How many samples we create. Each sample is a raytracing render.
# Use 1 for a raw preview, 10 for a draft and 100 or more for the final render
samples: 10
# [boolean=false] Make the background transparent
transparent_background: false
# [string='top'] [top,bottom,front,rear,right,left,z,Z,y,Y,x,X] Point of view.
# Compatible with `render_3d`
view: 'top'
# BoardView:
# This format allows simple pads and connections navigation, mainly for circuit debug.
# The output can be loaded using Open Board View (https://openboardview.org/)

View File

@ -188,63 +188,79 @@ def create_background_gradient(color1, color0):
nt.links.new(mx.outputs["Shader"], wo_is)
def apply_scene(file):
jscene = None
auto_camera = False
cam_ob = None
def apply_scene(file, n_view=0):
# Loads scene
if not file:
return
with open(file, 'rt') as f:
text = f.read()
print(text)
jscene = json.loads(text)
return 1
global jscene
if jscene is None:
with open(file, 'rt') as f:
text = f.read()
print(text)
jscene = json.loads(text)
scene = bpy.context.scene
# Select the board
print('- Select all')
bpy.ops.object.select_all(action='SELECT')
render = jscene.get('render')
if render:
# Make sure we start rotations from 0
bpy.context.active_object.rotation_euler = (0, 0, 0)
povs = jscene.get('point_of_view')
if povs:
pov = povs[n_view]
# Apply point of view
do_point_of_view(render, 'view')
do_point_of_view(pov, 'view')
# Apply extra rotations
do_rotate(render, 'rotate_x', 0)
do_rotate(render, 'rotate_y', 1)
do_rotate(render, 'rotate_z', 2)
do_rotate(pov, 'rotate_x', 0)
do_rotate(pov, 'rotate_y', 1)
do_rotate(pov, 'rotate_z', 2)
# Add a camera
auto_camera = False
camera = jscene.get('camera')
if not camera:
auto_camera = True
camera = {'name': 'kibot_camera', 'position': (0.0, 0.0, 10.0)}
name = camera.get('name', 'unknown')
pos = camera.get('position', (0, 0, 0))
print(f"- Creating camera {name} at {pos}")
cam_data = bpy.data.cameras.new(name)
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
global auto_camera
if not n_view:
# First time: create the camera
camera = jscene.get('camera')
if not camera:
auto_camera = True
camera = {'name': 'kibot_camera', 'position': (0.0, 0.0, 10.0)}
name = camera.get('name', 'unknown')
pos = camera.get('position', (0, 0, 0))
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
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)
# Add 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))
print(f"- Creating light {name} at {pos}")
light_data = bpy.data.lights.new(name, 'POINT')
light_ob = bpy.data.objects.new(name=name, object_data=light_data)
scene.collection.objects.link(light_ob)
light_ob.location = pos
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))
print(f"- Creating light {name} at {pos}")
light_data = bpy.data.lights.new(name, 'POINT')
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
if render:
render = jscene.get('render')
if render and not n_view:
scene.cycles.samples = render.get('samples', 10)
r = scene.render
r.engine = 'CYCLES'
@ -257,6 +273,7 @@ def apply_scene(file):
r.image_settings.color_mode = 'RGBA'
else:
create_background_gradient(render.get('background1', '#66667F'), render.get('background2', '#CCCCE5'))
return len(povs)
EXPORTERS = {'fbx': fbx_export,
@ -328,12 +345,24 @@ def main():
cut_boards=args.dont_cut_boards,
stack_boards=args.dont_stack_boards,
texture_dpi=args.texture_dpi)
# Apply the scene
apply_scene(args.scene)
# Do all the exports
for f, o in zip(args.format, args.output):
print(f"Exporting {o} in {f} format")
EXPORTERS[f](os.path.abspath(o))
# Apply the scene first scene
c_views = apply_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))
sys.exit(2)
per_pass = int(c_formats/c_views)
for n in range(c_views):
if n:
# Apply scene N
apply_scene(args.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]
# Do all the exports
for f, o in zip(formats, outputs):
print(f"Exporting {o} in {f} format")
EXPORTERS[f](os.path.abspath(o))
print("batch job finished, exiting")

View File

@ -9,7 +9,6 @@ Dependencies:
role: mandatory
version: 3.4.0
"""
from copy import copy
import json
import os
from tempfile import NamedTemporaryFile, TemporaryDirectory
@ -115,9 +114,6 @@ class BlenderLightOptions(Optionable):
class BlenderRenderOptions(Optionable):
""" Render parameters """
_views = {'top': 'z', 'bottom': 'Z', 'front': 'y', 'rear': 'Y', 'right': 'x', 'left': 'X'}
_rviews = {v: k for k, v in _views.items()}
def __init__(self, field=None):
super().__init__()
with document:
@ -134,6 +130,17 @@ class BlenderRenderOptions(Optionable):
""" First color for the background gradient """
self.background2 = "#CCCCE5"
""" Second color for the background gradient """
self._unkown_is_error = True
class BlenderPointOfViewOptions(Optionable):
""" Point of View parameters """
_views = {'top': 'z', 'bottom': 'Z', 'front': 'y', 'rear': 'Y', 'right': 'x', 'left': 'X'}
_rviews = {v: k for k, v in _views.items()}
def __init__(self, field=None):
super().__init__()
with document:
self.rotate_x = 0
""" Angle to rotate the board in the X axis, positive is clockwise [degrees] """
self.rotate_y = 0
@ -157,7 +164,6 @@ class BlenderRenderOptions(Optionable):
self._file_id = self.file_id
if not self._file_id:
self._file_id = '_'+self._rviews.get(self.view)
self._parent._expand_id += self._file_id
class PCB3DExportOptions(Base3DOptionsWithHL):
@ -195,9 +201,9 @@ class Blender_ExportOptions(BaseOptions):
""" [dict] Options for the camera.
If none specified KiBot will create a suitable camera """
self.render_options = BlenderRenderOptions
""" *[dict] Render and point of view options.
Controls how the render is done for the `render` output type,
and how the object is viewed by the camera """
""" *[dict] Controls how the render is done for the `render` output type """
self.point_of_view = BlenderPointOfViewOptions
""" *[dict|list(dict)] How the object is viewed by the camera """
super().__init__()
self._expand_id = '3D_blender'
self._unkown_is_error = True
@ -253,15 +259,24 @@ class Blender_ExportOptions(BaseOptions):
# Ensure we have some render options
if isinstance(self.render_options, type):
self.render_options = BlenderRenderOptions()
# Point of View, make sure we have a list and at least 1 element
if isinstance(self.point_of_view, type):
self.point_of_view = [BlenderPointOfViewOptions()]
elif isinstance(self.point_of_view, BlenderPointOfViewOptions):
self.point_of_view = [self.point_of_view]
def get_output_filename(self, o, output_dir):
def get_output_filename(self, o, output_dir, pov):
if o.type == 'render':
self._expand_ext = 'png'
elif o.type == 'blender':
self._expand_ext = 'blend'
else:
self._expand_ext = o.type
return self._parent.expand_filename(output_dir, o.output)
cur_id = self._expand_id
self._expand_id += pov._file_id
name = self._parent.expand_filename(output_dir, o.output)
self._expand_id = cur_id
return name
def get_targets(self, out_dir):
return [self.get_output_filename(o, out_dir) for o in self.outputs]
@ -393,21 +408,19 @@ class Blender_ExportOptions(BaseOptions):
scene['camera'] = {'name': self.camera.name,
'position': (self.camera.pos_x, self.camera.pos_y, self.camera.pos_z)}
ro = self.render_options
scr = {'samples': ro.samples,
'resolution_x': ro.resolution_x,
'resolution_y': ro.resolution_y,
'transparent_background': ro.transparent_background,
'background1': ro.background1,
'background2': ro.background2}
scene['render'] = scr
if ro.rotate_x:
scr['rotate_x'] = -ro.rotate_x
if ro.rotate_y:
scr['rotate_y'] = -ro.rotate_y
if ro.rotate_z:
scr['rotate_z'] = -ro.rotate_z
if ro.view:
scr['view'] = ro.view
scene['render'] = {'samples': ro.samples,
'resolution_x': ro.resolution_x,
'resolution_y': ro.resolution_y,
'transparent_background': ro.transparent_background,
'background1': ro.background1,
'background2': ro.background2}
povs = []
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})
scene['point_of_view'] = povs
text = json.dumps(scene, sort_keys=True, indent=2)
logger.debug('Scene:\n'+text)
f.write(text)
@ -433,10 +446,18 @@ class Blender_ExportOptions(BaseOptions):
if not pi.stack_boards:
cmd.append('--dont_stack_boards')
cmd.append('--format')
cmd.extend([o.type for o in self.outputs])
for _ in self.point_of_view:
for o in self.outputs:
cmd.append(o.type)
cmd.append('--output')
for o in self.outputs:
cmd.append(self.get_output_filename(o, self._parent.output_dir))
names = set()
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)
cmd.extend(['--scene', f.name])
cmd.append(pcb3d_file)
# Execute the command
@ -465,7 +486,6 @@ class Blender_Export(Base3D):
def get_conf_examples(name, layers, templates):
if not GS.check_tool(name, 'Blender'):
return None
outs = []
has_top = False
has_bottom = False
for la in layers:
@ -476,31 +496,18 @@ class Blender_Export(Base3D):
if not has_top and not has_bottom:
return None
register_xmp_import('PCB2Blender_2_1')
out_ops = {'pcb3d': '_PCB2Blender_2_1', 'outputs': [{'type': 'render'}, {'type': 'blender'}]}
povs = []
if has_top:
gb = {}
gb['name'] = 'basic_{}_top'.format(name)
gb['comment'] = '3D view from top (Blender)'
gb['type'] = name
gb['dir'] = '3D'
gb['options'] = copy(out_ops)
outs.append(gb)
gb = {}
gb['name'] = 'basic_{}_30deg'.format(name)
gb['comment'] = '3D view from 30 degrees (Blender)'
gb['type'] = name
gb['dir'] = '3D'
gb['output_id'] = '_30deg'
gb['options'] = copy(out_ops)
gb['options']['render_options'] = {'rotate_x': 30, 'rotate_z': -20}
outs.append(gb)
povs.append({'view': 'top'})
povs.append({'rotate_x': 30, 'rotate_z': -20, 'file_id': '_30deg'})
if has_bottom:
gb = {}
gb['name'] = 'basic_{}_bottom'.format(name)
gb['comment'] = '3D view from bottom (Blender)'
gb['type'] = name
gb['dir'] = '3D'
gb['options'] = copy(out_ops)
gb['options']['render_options'] = {'view': 'bottom'}
outs.append(gb)
return outs
povs.append({'view': 'bottom'})
gb = {}
gb['name'] = 'basic_{}'.format(name)
gb['comment'] = '3D view from top/30 deg/bottom (Blender)'
gb['type'] = name
gb['dir'] = '3D'
gb['options'] = {'pcb3d': '_PCB2Blender_2_1',
'outputs': [{'type': 'render'}, {'type': 'blender'}],
'point_of_view': povs}
return [gb]

View File

@ -22,6 +22,7 @@ outputs:
samples: 10
#resolution_x: 1920
#resolution_y: 1080
point_of_view:
rotate_x: 30
rotate_z: -20
# view: bottom

View File

@ -17,6 +17,7 @@ outputs:
samples: 10
#resolution_x: 1920
#resolution_y: 1080
point_of_view:
rotate_x: 30
rotate_z: -20
# view: bottom

View File

@ -0,0 +1,34 @@
# KiBot Blender export test 3
# Generating the PCB3D in the blender_export
# Multiple points of view
# src/kibot -vv -b tests/data/ArduinoLearningKitStarter.kicad_pcb -c tests/yaml_samples/blender_export_1.kibot.yaml -d r3d_lst
kibot:
version: 1
outputs:
- name: '3d_export'
comment: "Exports the PCB in blender format"
type: blender_export
options:
pcb3d:
show_components: ["RV1", "RV2", "U1", "U2", "U3"]
highlight: ["RV1"]
render_options:
transparent_background: true
samples: 10
#resolution_x: 1920
#resolution_y: 1080
point_of_view:
- rotate_x: 30
rotate_z: -20
# view: bottom
file_id: _30deg
- view: top
outputs:
- type: blender
- type: render
- name: 'navigate'
comment: "Browse the results"
type: navigate_results
run_by_default: false