[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:
parent
f860b7eb11
commit
a7063ea937
20
README.md
20
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ outputs:
|
|||
samples: 10
|
||||
#resolution_x: 1920
|
||||
#resolution_y: 1080
|
||||
point_of_view:
|
||||
rotate_x: 30
|
||||
rotate_z: -20
|
||||
# view: bottom
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ outputs:
|
|||
samples: 10
|
||||
#resolution_x: 1920
|
||||
#resolution_y: 1080
|
||||
point_of_view:
|
||||
rotate_x: 30
|
||||
rotate_z: -20
|
||||
# view: bottom
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue