├── .gitignore ├── AUTHORS.txt ├── LICENSE ├── README.md ├── __init__.py ├── bl_materials ├── __init__.py ├── material_cycles.py ├── material_luxcore.py └── utils.py ├── bl_optics ├── __init__.py ├── ccretro.py ├── lens.py ├── lens_system.py ├── mirror.py ├── sensor.py ├── siemens.py └── sqlens.py ├── bl_optomech ├── __init__.py ├── lens_aperture.py ├── lens_housing.py ├── post.py └── table.py ├── bl_raytrace ├── __init__.py └── trace_scene.py ├── bl_scenes ├── __init__.py └── luxcore_lights.py ├── bl_systems ├── __init__.py ├── load_zmx.py └── test_zemax_library.py ├── changes.txt ├── data └── ACES │ └── ACES_RICD_sensitivity_TB-2014-004.csv ├── fileparser ├── __init__.py └── zmx │ ├── __init__.py │ ├── import_zmx.py │ ├── surfaces.py │ └── zmx2oc.py ├── raytrace ├── coatingdata.py ├── element.py ├── glasscatalog.py ├── glasscatalog_data │ ├── glasscatalog_CDGM.pkl │ ├── glasscatalog_Hikari.pkl │ ├── glasscatalog_Hoya.pkl │ ├── glasscatalog_Ohara.pkl │ ├── glasscatalog_Schott.pkl │ └── glasscatalog_Sumita.pkl ├── intersect.py ├── intersect_asphere.py ├── lenssystem.py ├── paraxial.py ├── rayfan.py └── trace_sequential.py ├── surface ├── __init__.py ├── aspheric.py ├── cylindrical.py ├── flat.py ├── parabolic.py ├── radialprofiles.py ├── rim.py ├── sagsurface.py ├── spherical.py ├── surfaceutils.py └── toric.py └── utils ├── __init__.py ├── check_surface.py ├── constants.py ├── geometry └── __init__.py ├── lens_math.py └── paraxial └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.suo 3 | .vs/* 4 | .vscode/ 5 | *.py.bak 6 | *.bak 7 | 8 | bl_optics/test_zemax_library.py 9 | raytrace/ghostrender.cfg -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Authors: 2 | 3 | Johannes Hinrichs (CodeFHD) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 |

OptiCore

6 |

7 |

8 | Optical Elements Addon for Blender 9 |
10 | 11 | OptiCore is a Blender-addon to provide accurate and well-defined models of optical elements (lenses, mirrors, etc.) as well as a few selected optimechanical components (optical bench, posts). These models can not only help you to easily create renderings of optical laboratory experiments, but also stunning images involving caustics. 12 | 13 | This addon will primarily be maintained for the latest Blender release. The last commit was tested on Blender version 4.2.1 LTS. 14 | 15 |

16 | 17 |

18 | Introduction Video on YouTube 21 | 22 |

23 | 24 | 25 | The development of this addon was originally motivated by the need to create accurate visualisations of optical lenses. Modelling them in Blender via spheres and boolean operations can lead to artifacts for surfaces where only a small segemnt of the sphere is needed, unless very high (u,v)-number spheress are used. This, in turn, makes it very slow and RAM-intensive. 26 | With this addon, lenses are created using parametrisations that are used in lesn design. All computations are internally handled with 64-bit precision, including surface normals with edge-splits for the best possible performance. 27 | The name Opti"Core" is derived from the LuxCoreRender engine, which is a great rendering engine and inspired me to continue using Blender for lens system visualisations. 28 | It should be noted however, that OptiCore is completely independent from any rendering engine (including LuxCore), as its main purpose is to generate the geometric objects. An internal sequential ray-tracing feature is included (see below for further explanation), which is however not connected to Blenders rendering engines. Handling of materials of a third-party rendering engine, or the rendering engines included in Blender by default, are out of the scope of this addon at the present time. 29 | 30 | Below, you will find a brief description of the available optical elements and features. You can also find tutorial videos on my YouTube-channel [HowToPrint](https://www.youtube.com/@howtoprint6002). Please don't hesitate to open a GitHub-Issue or -Discussion if you have any questions or feature requests. 31 | 32 | ## Table of Contents 33 | * [Optical Elements](#optical-elements) 34 | * [Lens](#lens) 35 | * [Square Lens](#square-lens) 36 | * [Mirror](#mirror) 37 | * [Retroreflector](#retroreflector) 38 | * [Retroreflector](#siemens-star) 39 | * [Retroreflector](#zemax-file-import) 40 | * [Optomechanics](#optomechanics) 41 | * [Breadboard](#breadboard) 42 | * [Post](#post) 43 | 44 | ## Optical Elements 45 | 46 | ### Lens 47 | 48 | Creates an optical lens with flat, spherical or apsheric surfaces. Set surface radius = 0 for a flat surface. 49 | 50 | Singlet, Doublet and Triplet lenses are supported. (More than three elements cemented together are uncommon and are not covered at the moment due to the implications on code maintenance.) 51 | 52 | The component origin is placed at the on-axis intersection with the first surface. 53 | 54 | Aspheric surfaces are defined with a conical constant and three coefficients for polynominal terms. See the respective Wikipedia-page for an explanation: 55 | 56 | The lens can also be created as a cross-section model. 57 | 58 | A ray fan can be added to visualize the optical path of rays through the lens. This uses an internal, sequential ray tracing algorithm. Presently, flat, spherical and conic surfaces are supported; surfaces using the polynominal aspheric coefficients are not yet supported. 59 | 60 | ### Square Lens 61 | 62 | Creates a optical lens mesh with a quadratic outline. 63 | 64 | The feature set is currently reduces compared to regular lenses, i.e. spherical surfaces only and no option for a cross-section model. 65 | 66 | ### Mirror 67 | 68 | Creates an optical mirror mesh with a circular outline, spherical, parabolic or aspheric surface and a flat back. 69 | 70 | Parabolic Mirrors can be constructed with component origin at the focal point or mirror centre. For spherical mirrors, only the mirror centre is available at the moment. 71 | 72 | Parabolic mirrors can be constructed as off-axis parabolic mirrors. 73 | 74 | A hole along the collimated beam axis can be included. 75 | 76 | ### Retroreflector 77 | 78 | Creates an array of cubecorner retroreflectors. 79 | 80 | Features two "Retroreflector types": 81 | - "Cubecorner", which is how reflectors are typically manufactured, and 82 | - "Trirectangular tetrahedron", which is basically only the lower half. (Uncommon, but why not have it, looks cool...) 83 | 84 | Retroreflectors can be used in two ways: 85 | - with a mirror material, in which case you look directly onto the cubes. 86 | - with a glass/plastic material, in which case you look from the flat side. Retroreflection then occurs due to total internal reflection. 87 | 88 | A "Tip offset" can be specified to apply an offset to the base bottom vertices of the cube-corners. The range of [-1,1] corresponds to 5% of the "CubeCorner Spacing". This allows to mimic by-design imperfections of retroreflectors for traffic use - if they were perfect, light would be reflected from headlights back into headlight and not reach the human eye. 89 | 90 | Hint: slightly bevel all edges to give a realistic segemented look due to real imperfections of sharp corners. 91 | 92 | ### Siemens Star 93 | 94 | Creates a Siemens Star, a typical MTF or contrast test object. 95 | 96 | The structure can be created open ended, or surrounded by a solid cylinder wall and the Siemens Star cut out as a negative Structure. 97 | 98 | ### Zemax file import 99 | 100 | Zemax is a major professional optical design software. Its .zmx file format is common for exchanging optical designs, and many examples, e.g. from patent literature, are available in this format. 101 | The description of the file format is not published. It is in ASCII-format, so human readable, but based heavily on abbreviations and therefore not easy to interpret fully. OptiCore therefore features an import routine that implements the basic features, such as surface curvatures, standard aspheric surface, or mirrors. However, there may be many aspects that lead to an incorrect import into Blender. 102 | 103 | Besides the correct interpretation of the .zmx file, the ray tracing may fail because it contains obsolete optical glasses that are not in the included database. OptiCore currently includes a library that is generated from available, current data from glass manufacturers. 104 | 105 | In combination of these aspects, it is suggested to first check the internal, sequential ray-tracing to determine if a good focus spot is rendered and the system looks plausible. If not, checking the Blender system console can tell you if there are glass library errors. In this case, transfer to Blender materials may still be successful if you can identify the mateiral properties yourself. 106 | 107 | ## Optomechanics 108 | 109 | ### Breadboard 110 | 111 | Creates a rectangular plate with beveled holes and rounded corners, i.e. the top-plate of an optical table. 112 | 113 | Upon creation, the faces inside the holes are pre-selected. This allows you to easily assign a second material, e.g. to create a screw thread by shader. 114 | 115 | ### Post 116 | 117 | Creates an 0.5-inch optical post (Thorlabs-style), with a 4 mm hole at the top and 6 mm hole at the bottom, as well as a through hole. 118 | 119 | Warning: This mesh geometry with a through-hole appears to be very complicated to properly shade smooth. Artifacts may occur with high glossiness. Autosmooth appears to work better than split normals. 120 | 121 | Other post-sizes are to follow. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | bl_info = { 21 | "name": "OptiCore", 22 | "author": "Johannes Hinrichs (CodeFHD)", 23 | "version": (2, 0), 24 | "blender": (4, 2, 5), 25 | "location": "View3D > Add > Mesh", 26 | "description": "Adds a new optical element", 27 | "warning": "", 28 | "wiki_url": "", 29 | "category": "Add Mesh", 30 | } 31 | 32 | import numpy as np 33 | 34 | try: 35 | import bpy 36 | x = bpy.data # needed to fail with fake_bpy module, only on linux it seems 37 | in_blender = True 38 | except: 39 | # This allows me to import OptiCore as a separate module 40 | # and work with anything not in the bl_... submodules 41 | in_blender = False 42 | 43 | if in_blender: 44 | from bpy_extras.object_utils import AddObjectHelper, object_data_add # NOTE: have to import object_data_add here even though it is not used in this file, due to the way it is imported in other files. TODO: attempt to change it to absolute imports in those modules to clean this up. 45 | from .bl_optics import OBJECT_OT_add_lens, reset_lens_defaults, OBJECT_OT_add_sqlens, OBJECT_OT_add_mirror, OBJECT_OT_add_CCretro, OBJECT_OT_add_siemens 46 | from .bl_optomech import OBJECT_OT_add_table, OBJECT_OT_add_post 47 | from .bl_systems import OBJECT_OT_load_zmx, reset_loadzmx_faults #, OBJECT_OT_test_zmx 48 | class OBJECT_OT_reset_performance_variables(bpy.types.Operator, AddObjectHelper): 49 | """This function resets some variables that may have caused performance issues""" 50 | bl_idname = "mesh.reset_performance_variables" 51 | bl_label = "Reset Performance Critical Settings" 52 | bl_options = {'REGISTER', 'UNDO'} 53 | 54 | def draw(self, context): 55 | pass 56 | 57 | def execute(self, context): 58 | reset_lens_defaults() 59 | reset_loadzmx_faults() 60 | return {'FINISHED'} 61 | 62 | class OBJECT_MT_opticsmenu(bpy.types.Menu): 63 | bl_idname = 'OBJECT_MT_opticsmenu' 64 | bl_label = 'OptiCore Optics' 65 | 66 | def draw(self, context): 67 | self.layout.operator(OBJECT_OT_add_lens.bl_idname) 68 | # self.layout.operator(OBJECT_OT_add_sqlens.bl_idname) 69 | self.layout.operator(OBJECT_OT_add_mirror.bl_idname) 70 | self.layout.operator(OBJECT_OT_add_CCretro.bl_idname) 71 | self.layout.operator(OBJECT_OT_add_siemens.bl_idname) 72 | self.layout.operator(OBJECT_OT_load_zmx.bl_idname) 73 | # self.layout.operator(OBJECT_OT_test_zmx.bl_idname) 74 | self.layout.operator(OBJECT_OT_reset_performance_variables.bl_idname) 75 | 76 | class OBJECT_MT_optomechmenu(bpy.types.Menu): 77 | bl_idname = 'OBJECT_MT_optomechmenu' 78 | bl_label = 'OptiCore Optomechanics' 79 | 80 | def draw(self, context): 81 | self.layout.operator(OBJECT_OT_add_table.bl_idname) 82 | self.layout.operator(OBJECT_OT_add_post.bl_idname) 83 | 84 | def menu_func(self, context): 85 | self.layout.separator() 86 | self.layout.menu(OBJECT_MT_opticsmenu.bl_idname) 87 | self.layout.menu(OBJECT_MT_optomechmenu.bl_idname) 88 | 89 | # Registration 90 | 91 | def add_lens_button(self, context): 92 | self.layout.operator( 93 | OBJECT_OT_add_lens.bl_idname, 94 | text="Add Lens", 95 | icon='PLUGIN') 96 | """ 97 | def add_lsqens_button(self, context): 98 | self.layout.operator( 99 | OBJECT_OT_add_sqlens.bl_idname, 100 | text="Add Square Lens", 101 | icon='PLUGIN') 102 | """ 103 | def add_mirror_button(self, context): 104 | self.layout.operator( 105 | OBJECT_OT_add_mirror.bl_idname, 106 | text="Add Mirror", 107 | icon='PLUGIN') 108 | def add_table_button(self, context): 109 | self.layout.operator( 110 | OBJECT_OT_add_table.bl_idname, 111 | text="Add Table", 112 | icon='PLUGIN') 113 | def load_zmx_button(self, context): 114 | self.layout.operator( 115 | OBJECT_OT_load_zmx.bl_idname, 116 | text="Load lenses from .zmx file", 117 | icon='PLUGIN') 118 | """ 119 | def test_zmx_button(self, context): 120 | self.layout.operator( 121 | OBJECT_OT_test_zmx.bl_idname, 122 | text="Test zmx library", 123 | icon='PLUGIN') 124 | """ 125 | def reset_performance_button(self, context): 126 | self.layout.operator( 127 | OBJECT_OT_reset_performance_variables.bl_idname, 128 | text="Reset performance critical variables", 129 | icon='PLUGIN') 130 | 131 | """ 132 | # This allows you to right click on a button and link to the manual 133 | def add_lens_manual_map(): 134 | url_manual_prefix = "https://docs.blender.org/manual/en/dev/" 135 | url_manual_mapping = ( 136 | ("bpy.ops.mesh.add_lens", "editors/3dview/object"), 137 | ) 138 | return url_manual_prefix, url_manual_mapping 139 | def add_mirror_manual_map(): 140 | url_manual_prefix = "https://docs.blender.org/manual/en/dev/" 141 | url_manual_mapping = ( 142 | ("bpy.ops.mesh.add_mirror", "editors/3dview/object"), 143 | ) 144 | return url_manual_prefix, url_manual_mapping 145 | """ 146 | 147 | classes = (OBJECT_OT_reset_performance_variables, 148 | OBJECT_OT_add_lens, OBJECT_OT_add_mirror, OBJECT_OT_add_CCretro, # , OBJECT_OT_add_sqlens 149 | OBJECT_OT_add_siemens, OBJECT_OT_load_zmx, # OBJECT_OT_test_zmx, 150 | OBJECT_OT_add_table,OBJECT_OT_add_post) 151 | menus = (OBJECT_MT_opticsmenu, OBJECT_MT_optomechmenu) 152 | 153 | def register(): 154 | for cla in classes+menus: 155 | bpy.utils.register_class(cla) 156 | bpy.types.VIEW3D_MT_mesh_add.append(menu_func) 157 | 158 | def unregister(): 159 | for cla in classes+menus: 160 | bpy.utils.unregister_class(cla) 161 | bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) 162 | 163 | if __name__ == "__main__": 164 | register() 165 | -------------------------------------------------------------------------------- /bl_materials/__init__.py: -------------------------------------------------------------------------------- 1 | from .material_cycles import * 2 | from .material_luxcore import * 3 | from .utils import * -------------------------------------------------------------------------------- /bl_materials/material_cycles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import bpy 21 | 22 | from .utils import * 23 | from ..raytrace import glasscatalog 24 | 25 | 26 | def add_glass_cycles(glassname, n, cyclesType='ShaderNodeBsdfGlass', update=False): 27 | OC_material_name = get_OC_material_name(glassname, 'cycles') 28 | # Check if material already exists 29 | if OC_material_name in bpy.data.materials.keys() and not update: 30 | return bpy.data.materials.get(OC_material_name) 31 | elif update: 32 | material = bpy.data.materials.get(OC_material_name) 33 | else: 34 | material = bpy.data.materials.new(name=OC_material_name) 35 | material.use_nodes = True 36 | # set up a clear node tree 37 | nodes = material.node_tree.nodes 38 | nodes.clear() 39 | # define basic node components 40 | glass_node = nodes.new(type=cyclesType) 41 | output_node = nodes.new(type='ShaderNodeOutputMaterial') 42 | # GUI layout 43 | glass_node.location = (0, 0) 44 | output_node.location = (300, 0) 45 | # set parameters 46 | if cyclesType == 'ShaderNodeBsdfGlass': 47 | glass_node.inputs["Roughness"].default_value = 0.0 48 | glass_node.inputs["IOR"].default_value = n 49 | # connections 50 | material.node_tree.links.new(glass_node.outputs["BSDF"], output_node.inputs["Surface"]) 51 | return material 52 | 53 | def add_blackoutmaterial_cycles(objectname='LensEdge'): 54 | OC_material_name = get_OC_material_name(objectname, 'cycles') 55 | if OC_material_name in bpy.data.materials.keys(): 56 | return OC_material_name# bpy.data.materials.get(OC_material_name) 57 | else: 58 | material = bpy.data.materials.new(name=OC_material_name) 59 | material.use_nodes = True 60 | # set up a clear node tree 61 | nodes = material.node_tree.nodes 62 | nodes.clear() 63 | # define basic node components 64 | glass_node = nodes.new(type='ShaderNodeBsdfDiffuse') 65 | output_node = nodes.new(type='ShaderNodeOutputMaterial') 66 | # GUI layout 67 | glass_node.location = (0, 0) 68 | output_node.location = (300, 0) 69 | # set parameters 70 | glass_node.inputs["Color"].default_value = [0, 0, 0, 1] 71 | material.diffuse_color = [0, 0, 0, 1] 72 | # connections 73 | material.node_tree.links.new(glass_node.outputs["BSDF"], output_node.inputs["Surface"]) 74 | return OC_material_name 75 | 76 | def add_diffusematerial_cycles(objectname='LensDface', color=[1, 1, 1, 1], viewportcolor=[1, 1, 1, 1]): 77 | OC_material_name = get_OC_material_name(objectname, 'cycles') 78 | if OC_material_name in bpy.data.materials.keys(): 79 | return OC_material_name# bpy.data.materials.get(OC_material_name) 80 | else: 81 | material = bpy.data.materials.new(name=OC_material_name) 82 | material.use_nodes = True 83 | # set up a clear node tree 84 | nodes = material.node_tree.nodes 85 | nodes.clear() 86 | # define basic node components 87 | diffuse_node = nodes.new(type='ShaderNodeBsdfDiffuse') 88 | output_node = nodes.new(type='ShaderNodeOutputMaterial') 89 | # GUI layout 90 | diffuse_node.location = (0, 0) 91 | output_node.location = (300, 0) 92 | # set parameters 93 | diffuse_node.inputs["Color"].default_value = color 94 | material.diffuse_color = viewportcolor 95 | # connections 96 | material.node_tree.links.new(diffuse_node.outputs["BSDF"], output_node.inputs["Surface"]) 97 | return OC_material_name 98 | 99 | def glass_from_Element_cycles(ele, wl, mat_refract_only=False): 100 | materials_bulk = [] 101 | materials_interface = [] 102 | num_glasses = len(ele.data['type']) - 1 103 | n_list = [] 104 | 105 | if mat_refract_only: 106 | cyclesType = 'ShaderNodeBsdfRefraction' 107 | else: 108 | cyclesType = 'ShaderNodeBsdfGlass' 109 | 110 | # create materials 111 | for i in range(num_glasses): 112 | # bulk material 113 | glassname = ele.data['material'][i] 114 | glassname_is_dummy = glassname.startswith('FIXVALUE_') or glassname.startswith('___BLANK') 115 | n = glasscatalog.get_n(glassname, wl) 116 | n_list.append(n) 117 | if glassname_is_dummy: 118 | glassname = f'Glass-{n:.5f}' 119 | if mat_refract_only: 120 | glassname = glassname + '_refraction' 121 | material_exists, OC_material_name = check_OC_material(glassname, 'cycles') 122 | if not material_exists: 123 | _ = add_glass_cycles(glassname, n, cyclesType=cyclesType) 124 | materials_bulk.append(OC_material_name) 125 | 126 | # interface materials, if present 127 | if num_glasses > 1: 128 | for i in range(num_glasses - 1): 129 | n_ratio = n_list[i+1]/n_list[i] 130 | glassname1 = ele.data['material'][i] 131 | glassname2 = ele.data['material'][i+1] 132 | glassname1_is_dummy = glassname1.startswith('FIXVALUE_') or glassname1.startswith('___BLANK') 133 | glassname2_is_dummy = glassname2.startswith('FIXVALUE_') or glassname2.startswith('___BLANK') 134 | if (glassname1_is_dummy or glassname2_is_dummy): 135 | glassname = f'GlassIF-{n_ratio:.5f}' 136 | else: 137 | glassname = f'GlassIF-{glassname1}-{glassname2}' 138 | material_exists, OC_material_name = check_OC_material(glassname, 'cycles') 139 | if not material_exists: 140 | _ = add_glass_cycles(glassname, n_ratio, cyclesType=cyclesType) 141 | materials_interface.append(OC_material_name) 142 | 143 | return materials_bulk, materials_interface -------------------------------------------------------------------------------- /bl_materials/material_luxcore.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import bpy 21 | 22 | from .utils import * 23 | from ..raytrace import glasscatalog 24 | 25 | 26 | def add_glass_luxcore(glassname, n, luxcoreType='LuxCoreNodeMatGlass', update=False, reflection_color=[0, 0, 0]): 27 | OC_material_name = get_OC_material_name(glassname, 'luxcore') 28 | # Check if material already exists 29 | if OC_material_name in bpy.data.materials.keys() and not update: 30 | return bpy.data.materials.get(OC_material_name) 31 | elif update: 32 | material = bpy.data.materials.get(OC_material_name) 33 | else: 34 | material = bpy.data.materials.new(name=OC_material_name) 35 | material.use_nodes = True 36 | # set up a clear node tree 37 | luxcore_tree = bpy.data.node_groups.new(name='Nodes_' + OC_material_name, type="luxcore_material_nodes") 38 | material.luxcore.node_tree = luxcore_tree 39 | nodes = luxcore_tree.nodes 40 | nodes.clear() 41 | # define basic node components 42 | glass_node = nodes.new(type=luxcoreType) 43 | output_node = nodes.new(type='LuxCoreNodeMatOutput') 44 | # GUI layout 45 | glass_node.location = (0, 0) 46 | output_node.location = (300, 0) 47 | # set parameters 48 | glass_node.inputs["Transmission Color"].default_value = [1, 1, 1] 49 | glass_node.inputs["Reflection Color"].default_value = reflection_color 50 | glass_node.inputs["IOR"].default_value = n 51 | # connections 52 | luxcore_tree.links.new(glass_node.outputs["Material"], output_node.inputs["Material"]) 53 | return material 54 | 55 | def add_blackoutmaterial_luxcore(objectname='LensEdge'): 56 | OC_material_name = get_OC_material_name(objectname, 'luxcore') 57 | if OC_material_name in bpy.data.materials.keys(): 58 | return OC_material_name# bpy.data.materials.get(OC_material_name) 59 | else: 60 | material = bpy.data.materials.new(name=OC_material_name) 61 | material.use_nodes = True 62 | # set up a clear node tree 63 | luxcore_tree = bpy.data.node_groups.new(name='Nodes_' + OC_material_name, type="luxcore_material_nodes") 64 | material.luxcore.node_tree = luxcore_tree 65 | nodes = luxcore_tree.nodes 66 | nodes.clear() 67 | # define basic node components 68 | matte_node = nodes.new(type='LuxCoreNodeMatMatte') 69 | output_node = nodes.new(type='LuxCoreNodeMatOutput') 70 | # GUI layout 71 | matte_node.location = (0, 0) 72 | output_node.location = (300, 0) 73 | # set parameters 74 | matte_node.inputs["Diffuse Color"].default_value = [0, 0, 0] 75 | material.diffuse_color = [0, 0, 0, 1] 76 | # connections 77 | luxcore_tree.links.new(matte_node.outputs["Material"], output_node.inputs["Material"]) 78 | return OC_material_name 79 | 80 | def add_diffusematerial_luxcore(objectname='LensDface', color=[1, 1, 1], viewportcolor=[1, 1, 1, 1]): 81 | OC_material_name = get_OC_material_name(objectname, 'luxcore') 82 | if OC_material_name in bpy.data.materials.keys(): 83 | return OC_material_name# bpy.data.materials.get(OC_material_name) 84 | else: 85 | material = bpy.data.materials.new(name=OC_material_name) 86 | material.use_nodes = True 87 | # set up a clear node tree 88 | luxcore_tree = bpy.data.node_groups.new(name='Nodes_' + OC_material_name, type="luxcore_material_nodes") 89 | material.luxcore.node_tree = luxcore_tree 90 | nodes = luxcore_tree.nodes 91 | nodes.clear() 92 | # define basic node components 93 | diffuse_node = nodes.new(type='LuxCoreNodeMatMatte') 94 | output_node = nodes.new(type='LuxCoreNodeMatOutput') 95 | # GUI layout 96 | diffuse_node.location = (0, 0) 97 | output_node.location = (300, 0) 98 | # set parameters 99 | diffuse_node.inputs["Diffuse Color"].default_value = color 100 | material.diffuse_color = viewportcolor 101 | # connections 102 | luxcore_tree.links.new(diffuse_node.outputs["Material"], output_node.inputs["Material"]) 103 | return OC_material_name 104 | 105 | def glass_from_Element_luxcore(ele, wl, mat_refract_only=False): 106 | materials_bulk = [] 107 | materials_interface = [] 108 | num_glasses = len(ele.data['type']) - 1 109 | n_list = [] 110 | 111 | luxcoreType = 'LuxCoreNodeMatGlass' 112 | if mat_refract_only: 113 | reflection_color = [0, 0, 0] 114 | else: 115 | reflection_color = [1, 1, 1] 116 | 117 | # create materials 118 | for i in range(num_glasses): 119 | # bulk material 120 | glassname = ele.data['material'][i] 121 | glassname_is_dummy = glassname.startswith('FIXVALUE_') or glassname.startswith('___BLANK') 122 | n = glasscatalog.get_n(glassname, wl) 123 | n_list.append(n) 124 | if glassname_is_dummy: 125 | glassname = f'Glass-{n:.5f}' 126 | if mat_refract_only: 127 | glassname = glassname + '_refraction' 128 | material_exists, OC_material_name = check_OC_material(glassname, 'luxcore') 129 | if not material_exists: 130 | _ = add_glass_luxcore(glassname, n, luxcoreType=luxcoreType, reflection_color=reflection_color) 131 | materials_bulk.append(OC_material_name) 132 | 133 | # interface materials, if present 134 | if num_glasses > 1: 135 | for i in range(num_glasses - 1): 136 | n_ratio = n_list[i+1]/n_list[i] 137 | glassname1 = ele.data['material'][i] 138 | glassname2 = ele.data['material'][i+1] 139 | glassname1_is_dummy = glassname1.startswith('FIXVALUE_') or glassname1.startswith('___BLANK') 140 | glassname2_is_dummy = glassname2.startswith('FIXVALUE_') or glassname2.startswith('___BLANK') 141 | if (glassname1_is_dummy or glassname2_is_dummy): 142 | glassname = f'GlassIF-{n_ratio:.5f}' 143 | else: 144 | glassname = f'GlassIF-{glassname1}-{glassname2}' 145 | material_exists, OC_material_name = check_OC_material(glassname, 'luxcore') 146 | if not material_exists: 147 | _ = add_glass_luxcore(glassname, n_ratio, luxcoreType=luxcoreType, reflection_color=reflection_color) 148 | materials_interface.append(OC_material_name) 149 | 150 | return materials_bulk, materials_interface -------------------------------------------------------------------------------- /bl_materials/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import bpy 21 | 22 | def clear_all_materials(): 23 | # bpy.ops.outliner.orphans_purge() # TODO: This would be useful but might be messing with users scenes because it deletes too much. --> Limit to OC_ meshes and materials 24 | materials = bpy.data.materials 25 | for material in materials[:]: # use a copy so that removal doesn't mess with the iteration 26 | if material.users == 0 and material.name.startswith('OC_'): 27 | materials.remove(material) 28 | 29 | def get_OC_material_name(material_name, engine): 30 | OC_material_name = 'OC_' + material_name + '_' + engine 31 | return OC_material_name 32 | 33 | 34 | def check_OC_material(material_name, engine): 35 | OC_material_name = get_OC_material_name(material_name, engine) 36 | material_exists = OC_material_name in bpy.data.materials.keys() 37 | return material_exists, OC_material_name -------------------------------------------------------------------------------- /bl_optics/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | from .lens import * 21 | from .mirror import * 22 | from .sqlens import * 23 | from .ccretro import * 24 | from .siemens import * 25 | from .sensor import * -------------------------------------------------------------------------------- /bl_optics/lens_system.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import bpy 4 | from bpy.props import FloatProperty, IntProperty, EnumProperty, StringProperty, BoolProperty, FloatVectorProperty 5 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 6 | 7 | from . import OBJECT_OT_add_lens, OBJECT_OT_add_sqlens#, OBJECT_OT_add_mirror, OBJECT_OT_add_CCretro 8 | 9 | class OBJECT_OT_lens_system(bpy.types.Operator, AddObjectHelper): 10 | """Create a new Mesh Object""" 11 | bl_idname = "mesh.add_lenssystem" 12 | bl_label = "Lens System" 13 | bl_options = {'REGISTER', 'UNDO'} 14 | 15 | def create_system(system_name="Lens_System"): 16 | #create basic settings 17 | lens_dict = {} 18 | 19 | #add empty and name 20 | bpy.ops.object.add() 21 | bpy.context.active_object.name = system_name 22 | #if there was a duplicate swap around 23 | if not bpy.context.active_object.name == system_name: 24 | system_name = bpy.context.active_object.name 25 | #create custom property to say this is a lens system 26 | bpy.context.active_object["IS_OPTICORE_SYSTEM"] = "True" 27 | bpy.context.active_object["OPTICORE_SYSTEM_NAME"] = system_name 28 | 29 | #create individual lenses 30 | #and add name to empty custom property list 31 | 32 | 33 | 34 | def update_system(): 35 | pass 36 | 37 | 38 | def execute(self, context): 39 | create_system(self, context) 40 | return {'FINISHED'} 41 | 42 | def create_system(self, context): 43 | pass -------------------------------------------------------------------------------- /bl_optics/mirror.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | import bpy 23 | from bpy.types import Operator, ParticleSettingsTextureSlot 24 | from bpy.props import FloatProperty, IntProperty, EnumProperty, StringProperty, BoolProperty, FloatVectorProperty 25 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 26 | 27 | from .. import surface as sfc 28 | from .. import object_data_add 29 | from .. import utils 30 | 31 | class OBJECT_OT_add_mirror(Operator, AddObjectHelper): 32 | """Create a new Mesh Object""" 33 | bl_idname = "mesh.add_mirror" 34 | bl_label = "Mirror" 35 | bl_options = {'REGISTER', 'UNDO'} 36 | 37 | mtype : EnumProperty( 38 | name="Surface Shape", 39 | items = (("parabolic","Parabolic",""), 40 | ("spherical","Spherical",""), 41 | ("aspheric", "Aspheric", "")), 42 | default = "parabolic", 43 | description="Shape of Mirror Surface", 44 | ) 45 | opos : EnumProperty( 46 | name="Origin position", 47 | items = (("FP","Focal Point",""), 48 | ("MC","Mirror Center","")), 49 | default = "FP", 50 | description="Position of the Mesh Origin w.r.t. optical properties", 51 | ) 52 | rad : FloatProperty( 53 | name="Surface Radius", 54 | default = 12., 55 | description="Radius of Curvature of Mirror Surface", 56 | unit = "LENGTH", 57 | ) 58 | num1 : IntProperty( 59 | name="N1", 60 | default = 32, 61 | description="Number of radial vertices", 62 | min=3, 63 | ) 64 | num2 : IntProperty( 65 | name="N2", 66 | default = 64, 67 | description="Nubmer of angular vertices", 68 | min=3, 69 | ) 70 | mirrorradius : FloatProperty( 71 | name="Mirror Radius", 72 | default = 3., 73 | description="Mirror outer radius", 74 | unit = "LENGTH", 75 | ) 76 | centerthickness : FloatProperty( 77 | name="Back Thickness", 78 | default = 1., 79 | description="Thickness at thinnest point", 80 | unit = "LENGTH", 81 | ) 82 | ASPDEG = 3 83 | k : FloatProperty( 84 | name="k", 85 | default = 0., 86 | description="Aspheric conical constant", 87 | ) 88 | A : FloatVectorProperty( 89 | name="A", 90 | #default = (0.,0.,0.), 91 | default = list([0. for i in range(ASPDEG)]), 92 | description="Aspheric correction coefficients", 93 | size = ASPDEG, 94 | ) 95 | theta : FloatProperty( 96 | name="Offset Angle", 97 | default = 0., 98 | description="Offset angle for off-axis mirror", 99 | unit = "ROTATION", 100 | ) 101 | material_name : StringProperty( 102 | name="Material", 103 | default="", 104 | ) 105 | shade_smooth : BoolProperty( 106 | name="Smooth Shading", 107 | default=True, 108 | ) 109 | smooth_type : BoolProperty( 110 | name="Use Custom Normals", 111 | default=True, 112 | ) 113 | cent_hole : BoolProperty( 114 | name="Central Hole", 115 | default=False, 116 | ) 117 | hole_rad : FloatProperty( 118 | name="HoleRadius", 119 | default = 0.1, 120 | description="Radius of Curvature of Mirror Surface", 121 | min = 0.01, 122 | unit = "LENGTH", 123 | ) 124 | display_edit : BoolProperty( 125 | name="Display Edit Mode", 126 | default=False, 127 | ) 128 | 129 | def draw(self, context): 130 | layout = self.layout 131 | #scene = context.scene 132 | layout.prop(self, 'mtype') 133 | if self.mtype == 'parabolic': 134 | layout.prop(self, 'opos') 135 | layout.prop(self, 'rad') 136 | layout.prop(self, 'mirrorradius') 137 | layout.prop(self, 'centerthickness') 138 | if self.mtype == 'aspheric': 139 | layout.prop(self, 'k') 140 | layout.prop(self, 'A') 141 | if self.mtype == 'parabolic': 142 | layout.prop(self, 'theta') 143 | layout.prop(self, 'num1') 144 | layout.prop(self, 'num2') 145 | layout.prop_search(self, "material_name", bpy.data, "materials", icon="NONE") 146 | #layout.prop(self, 'material_name') 147 | layout.prop(self, 'shade_smooth') 148 | # if self.shade_smooth: 149 | # layout.prop(self, 'smooth_type') 150 | layout.prop(self, 'cent_hole') 151 | if self.cent_hole: 152 | layout.prop(self, 'hole_rad') 153 | layout.prop(self, 'display_edit') 154 | 155 | def execute(self, context): 156 | add_mirror(self, context) 157 | return {'FINISHED'} 158 | 159 | def get_default_paramdict_mirror(): 160 | """ 161 | This functions returns a parameter dictionary filled with some default values. 162 | This can be used as a template for external calls to add_lens() 163 | when it is desired to only use a subset of the possible varaibles 164 | """ 165 | paramdict = {} 166 | paramdict['rad'] = 12. 167 | paramdict['num1'] = 32 168 | paramdict['num2'] = 64 169 | paramdict['mirrorradius'] = 3. 170 | paramdict['centerthickness'] = 1. 171 | paramdict['mtype'] = 'parabolic' 172 | paramdict['k'] = 0. 173 | paramdict['A'] = list([0. for i in range(3)]) 174 | paramdict['theta'] = 0. 175 | paramdict['opos'] = "FP" 176 | paramdict['cent_hole'] = False 177 | paramdict['hole_rad'] = 0.1 178 | paramdict['material_name'] = "" 179 | paramdict['shade_smooth'] = True 180 | paramdict['smooth_type'] = True 181 | paramdict['display_edit'] = False 182 | paramdict['flipdirection'] = 1 183 | return paramdict 184 | 185 | def add_mirror(self, context, paramdict=None): 186 | if paramdict is None: 187 | srad = self.rad 188 | N1 = self.num1 189 | N2 = self.num2 190 | mrad = self.mirrorradius 191 | CT = self.centerthickness 192 | surftype = self.mtype 193 | k = self.k 194 | A = self.A 195 | theta = self.theta*180/np.pi 196 | opos = self.opos 197 | hrad = 0 198 | cent_hole = self.cent_hole 199 | if self.cent_hole: 200 | hrad = self.hole_rad 201 | if hrad > 0.99*mrad: 202 | hrad = 0.99*mrad 203 | material_name = self.material_name 204 | shade_smooth = self.shade_smooth 205 | smooth_type = self.smooth_type 206 | display_edit = self.display_edit 207 | flipdirection = False 208 | else: 209 | srad = paramdict['rad'] 210 | N1 = paramdict['num1'] 211 | N2 = paramdict['num2'] 212 | mrad = paramdict['mirrorradius'] 213 | CT = paramdict['centerthickness'] 214 | surftype = paramdict['mtype'] 215 | k = paramdict['k'] 216 | A = paramdict['A'] 217 | theta = paramdict['theta']*180/np.pi 218 | opos = paramdict['opos'] 219 | hrad = 0 220 | cent_hole = paramdict['cent_hole'] 221 | if cent_hole: 222 | hrad = paramdict['hole_rad'] 223 | if hrad > 0.99*mrad: 224 | hrad = 0.99*mrad 225 | material_name = paramdict['material_name'] 226 | shade_smooth = paramdict['shade_smooth'] 227 | smooth_type = paramdict['smooth_type'] 228 | display_edit = paramdict['display_edit'] 229 | flipdirection = paramdict['flipdirection'] 230 | 231 | #check surface radius for consistency 232 | if surftype == 'spherical': 233 | if abs(srad) < mrad: srad = 0 234 | # if not utils.check_surface(np.abs(srad), mrad): srad=0 235 | 236 | # no hole for aspheric surface 237 | if surftype == 'aspheric': 238 | cent_hole = False 239 | 240 | #compute mirror surface 241 | if srad == 0: #flat surface case 242 | verts, faces, normals = sfc.add_flat_surface(mrad, N1, N2, hole=cent_hole, hrad=hrad) 243 | xadd = 0 244 | zOA = 0 245 | else: 246 | if surftype == 'spherical': 247 | verts, faces, normals = sfc.add_spherical_surface(-srad, mrad, N1, N2,hole=cent_hole,hrad=hrad) 248 | xadd = 0 249 | zOA = 0 250 | if srad < 0: 251 | zOA = -np.abs(srad)+np.sqrt(srad**2-mrad**2) 252 | elif surftype == 'aspheric': 253 | verts, faces, normals = sfc.add_sagsurface_circular(-srad, k, A, mrad, N1, N2, nVerts=0, surftype='aspheric') 254 | #verts, faces, normals = sfc.add_aspheric_surface(-srad, k, A, mrad, N1, N2, 1) 255 | xadd = 0 256 | zOA = 0 257 | elif surftype == 'parabolic': 258 | verts, faces, xadd, zOA, normals = sfc.add_parabolic_surface(-srad, mrad, N1, N2, theta, orig=opos,hole=cent_hole,hrad=hrad) 259 | 260 | # reorder the coordinate system to Blender covnention 261 | verts = [[t[2], t[0], t[1]] for t in verts] 262 | normals = [[t[2], t[0], t[1]] for t in normals] 263 | 264 | nVerts = len(verts) 265 | 266 | #add rear surface 267 | verts2, faces2, normals2 = sfc.add_flat_surface(mrad, N1, N2, CT-zOA, xadd, nVerts=nVerts,hole=cent_hole,hrad=hrad) 268 | 269 | # reorder the coordinate system to Blender covnention 270 | verts2 = [[t[2], t[0], t[1]] for t in verts2] 271 | normals2 = [[t[2], t[0], t[1]] for t in normals2] 272 | 273 | # flip normals for last surface 274 | faces2 = [df[::-1] for df in faces2] 275 | normals2 = [(-t[0], -t[1], -t[2]) for t in normals2] 276 | 277 | nVerts2 = len(verts2) 278 | 279 | verts = verts + verts2 280 | faces = faces + faces2 281 | normals = normals + normals2 282 | 283 | nVerts_tot = len(verts) 284 | 285 | del verts2 286 | del faces2 287 | 288 | #add side 289 | for j in range(N2): 290 | fi1 = nVerts + (j+1)%(N2) + N2*cent_hole 291 | fi2 = nVerts + j + N2*cent_hole 292 | fi3 = fi2 - N2*(1 + cent_hole) 293 | fi4 = fi1 - N2*(1 + cent_hole) 294 | faces.append([fi4,fi3,fi2,fi1]) 295 | 296 | normalsside = sfc.get_ringnormals(N2) 297 | normalsside = [[t[2], t[0], t[1]] for t in normalsside] 298 | 299 | #fill hole 300 | normalshole = [] #in case there is no hole 301 | if cent_hole: 302 | for j in range(N2): 303 | fi2 = j 304 | fi1 = (j+1)%N2 305 | fi4 = nVerts + (j+1)%N2 306 | fi3 = nVerts + j 307 | faces.append([fi4,fi3,fi2,fi1]) 308 | 309 | normalshole = sfc.get_ringnormals(N2) 310 | normalshole = [(-n[2], -n[0], -n[1]) for n in normalshole] 311 | 312 | #create mesh from verts and faces 313 | mesh = bpy.data.meshes.new(name = 'OC_Mirror') 314 | edges = [] # edges are not explicitly created here so we pass an empty list 315 | mesh.from_pydata(verts, edges, faces) 316 | # useful for development when the mesh may be invalid. 317 | #mesh.validate(verbose=True) 318 | obj = object_data_add(context, mesh, operator=self) 319 | 320 | #assign matierals 321 | if material_name in bpy.data.materials: 322 | mat = bpy.data.materials[material_name] 323 | obj.data.materials.append(mat) 324 | 325 | #apply smooth shading 326 | if shade_smooth: 327 | bpy.ops.object.shade_smooth() 328 | if smooth_type: #assign custom normals 329 | bpy.ops.mesh.customdata_custom_splitnormals_clear() 330 | bpy.ops.mesh.customdata_custom_splitnormals_add() 331 | 332 | cn1, cn2, cn3, cn4 = [], [], [], [] 333 | #mirror surface 334 | if srad == 0: 335 | if cent_hole: 336 | nloopsface = 4*N2 337 | else: 338 | nloopsface = N2 339 | else: 340 | if cent_hole: 341 | nloopsface = N2*4*(N1-1) 342 | else: 343 | nloopsface = N2*(3 + 4*(N1-1)) 344 | for i in range(nloopsface): 345 | vi = mesh.loops[i].vertex_index 346 | cn1.append(normals[vi]) 347 | #rear surface 348 | if cent_hole: 349 | nloopsrear = 4*N2 350 | else: 351 | nloopsrear = N2 352 | for i in range(nloopsrear): 353 | vi = mesh.loops[i+nloopsface].vertex_index 354 | cn2.append(normals[vi]) 355 | #side 356 | nloopsside = 4*N2 357 | for i in range(nloopsside): 358 | vi = mesh.loops[i+nloopsface+nloopsrear].vertex_index 359 | if vi < nVerts: 360 | vi = vi - nVerts + N2 361 | else: 362 | vi = (vi - nVerts)%N2 363 | cn3.append(normalsside[vi]) 364 | #hole 365 | if cent_hole: 366 | nloopshole = 4*N2 367 | for i in range(nloopshole): 368 | vi = mesh.loops[i+nloopsface+nloopsrear+nloopsside].vertex_index 369 | if vi < nVerts: 370 | pass 371 | else: 372 | vi = vi - nVerts_tot + N2 373 | cn4.append(normalshole[vi]) 374 | 375 | mesh.normals_split_custom_set(cn1 + cn2 + cn3 + cn4) 376 | 377 | if flipdirection == -1: 378 | obj.rotation_euler = [0,0,np.pi] 379 | 380 | #for testing 381 | if display_edit: 382 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 383 | bpy.ops.mesh.select_all(action='DESELECT') -------------------------------------------------------------------------------- /bl_optics/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | # from mathutils import Vector 23 | 24 | import bpy 25 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 26 | 27 | from ..bl_materials import add_diffusematerial_cycles, add_diffusematerial_luxcore 28 | 29 | def add_sensor(self, context, lx, ly, zsensor, thicksensor=False, sensorthickness=None): 30 | verts = [] 31 | edges = [] 32 | faces = [] 33 | 34 | if thicksensor and sensorthickness is None: 35 | sensorthickness = max(lx, ly)/20 36 | 37 | # front face 38 | verts.append([0, -lx, -ly]) 39 | verts.append([0, lx, -ly]) 40 | verts.append([0, lx, ly]) 41 | verts.append([0, -lx, ly]) 42 | faces.append([0, 1, 2, 3]) 43 | 44 | if thicksensor: 45 | # rear face 46 | verts.append([-sensorthickness, -lx, -ly]) 47 | verts.append([-sensorthickness, lx, -ly]) 48 | verts.append([-sensorthickness, lx, ly]) 49 | verts.append([-sensorthickness, -lx, ly]) 50 | faces.append([7, 6, 5, 4]) 51 | # sides 52 | faces.append([0, 4, 5, 1]) # bottom 53 | faces.append([1, 5, 6, 2]) # right 54 | faces.append([2, 6, 7, 3]) # top 55 | faces.append([0, 3, 7, 4]) # left 56 | 57 | #create mesh from verts and faces 58 | mesh = bpy.data.meshes.new(name = 'OC_Sensor') 59 | mesh.from_pydata(verts, edges, faces) 60 | # useful for development when the mesh may be invalid. 61 | #mesh.validate(verbose=True) 62 | obj = object_data_add(context, mesh, operator=self) 63 | 64 | using_cycles = context.scene.render.engine == 'CYCLES' 65 | using_luxcore = context.scene.render.engine == 'LUXCORE' 66 | 67 | if using_cycles: 68 | materialname_sensor = add_diffusematerial_cycles(objectname='Sensor') 69 | elif using_luxcore: 70 | materialname_sensor = add_diffusematerial_luxcore(objectname='Sensor') 71 | if using_cycles or using_luxcore: 72 | material_sensor = bpy.data.materials[materialname_sensor] 73 | ob = bpy.context.active_object 74 | ob.data.materials.append(material_sensor) -------------------------------------------------------------------------------- /bl_optics/siemens.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from mathutils import Vector 23 | 24 | import bpy 25 | from bpy.props import FloatProperty, IntProperty, EnumProperty, StringProperty, BoolProperty, FloatVectorProperty 26 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 27 | 28 | from .. import surface as sfc 29 | from .. import object_data_add 30 | from .. import utils 31 | 32 | class OBJECT_OT_add_siemens(bpy.types.Operator, AddObjectHelper): 33 | """Create a new Mesh Object""" 34 | bl_idname = "mesh.add_siemensstar" 35 | bl_label = "Siemens Star" 36 | bl_options = {'REGISTER', 'UNDO'} 37 | 38 | openended : BoolProperty( 39 | name="Open Ended", 40 | default=True, 41 | ) 42 | rad1 : FloatProperty( 43 | name="Star Radius", 44 | default = 12., 45 | description="Radius of the Siemens star", 46 | unit = "LENGTH", 47 | ) 48 | rad2 : FloatProperty( 49 | name="Outside Radius", 50 | default = 16., 51 | description="Radius of the full disk", 52 | unit = "LENGTH", 53 | ) 54 | thickness : FloatProperty( 55 | name="Thickness", 56 | default = 2., 57 | description="Thickness of the Siemens star", 58 | unit = "LENGTH", 59 | ) 60 | num1 : IntProperty( 61 | name="N", 62 | default = 32, 63 | description="Number of line pairs", 64 | min=4, 65 | ) 66 | num2 : IntProperty( 67 | name="Rim Resolution", 68 | default = 4, 69 | description="Resolution of the outer rim", 70 | min=1, 71 | ) 72 | 73 | def draw(self, context): 74 | layout = self.layout 75 | # Location 76 | col = layout.column(align=True) 77 | col.label(text="Location") 78 | col.prop(self, 'location', text="") 79 | # Rotation 80 | col = layout.column(align=True) 81 | col.label(text="Rotation") 82 | col.prop(self, 'rotation', text="") 83 | scene = context.scene 84 | #layout.prop(self, 'rshape') 85 | layout.prop(self, 'openended') 86 | layout.prop(self, 'rad1') 87 | if not self.openended: 88 | layout.prop(self, 'rad2') 89 | layout.prop(self, 'thickness') 90 | layout.prop(self, 'num1') 91 | layout.prop(self, 'num2') 92 | 93 | def execute(self, context): 94 | add_siemensstar(self, context) 95 | return {'FINISHED'} 96 | 97 | def add_siemensstar(self, context): 98 | verts = [] 99 | edges = [] 100 | faces = [] 101 | 102 | #basic parameters 103 | OE = self.openended 104 | Nlines = self.num1 105 | Nseg = self.num2 106 | Nouter = 2*Nlines*Nseg 107 | t = self.thickness 108 | r1 = self.rad1 109 | r2 = self.rad2 110 | 111 | if not OE: 112 | if r1 > r2: r2, r1 = r1, r2 113 | 114 | #center vertex 115 | verts.append(Vector((0,0,0))) 116 | 117 | #Siemens star vertices 118 | for i in range(2*Nlines): 119 | alpha = 2*np.pi/2/Nlines*i 120 | ca = np.cos(alpha) 121 | sa = np.sin(alpha) 122 | verts.append(Vector((r1*sa,r1*ca,0))) 123 | if i%2 == 0: 124 | for j in range (1,Nseg): 125 | alpha = 2*np.pi/2/Nlines*(i + j/Nseg) 126 | ca = np.cos(alpha) 127 | sa = np.sin(alpha) 128 | verts.append(Vector((r1*sa,r1*ca,0))) 129 | 130 | nVerts0 = len(verts) 131 | #outer ring vertices #if not openended 132 | if not OE: 133 | for i in range(Nouter): 134 | alpha = 2*np.pi/Nouter*i 135 | ca = np.cos(alpha) 136 | sa = np.sin(alpha) 137 | verts.append(Vector((r2*sa,r2*ca,0))) 138 | 139 | #duplicate all vertices for top/bottom 140 | nVerts = len(verts) 141 | verts2 = [] 142 | for v in verts: 143 | verts2.append(Vector((v[0],v[1],t))) 144 | verts = verts + verts2 145 | 146 | #faces inside star 147 | for i in range(2*Nlines): 148 | fi1 = 0 149 | fi2 = nVerts 150 | fi3 = 1 + (i+1)//2*Nseg + (i)//2 151 | fi4 = fi3 + nVerts 152 | if OE: 153 | dir = 1 - 2*(i%2) 154 | else: 155 | dir = 2*(i%2) - 1 156 | faces.append([fi1,fi2,fi4,fi3][::dir]) 157 | 158 | #faces outside star 159 | for i in range(Nlines): 160 | for j in range(Nseg): 161 | f = [] 162 | f.append(i*(Nseg+1) + j + 2) 163 | f.append(i*(Nseg+1) + j + 1) 164 | f.append(i*(Nseg+1) + j + 1 + nVerts) 165 | f.append(i*(Nseg+1) + j + 2 + nVerts) 166 | if OE: 167 | faces.append(f) 168 | else: 169 | faces.append(f[::-1]) 170 | 171 | #top/bottom faces 172 | if OE: 173 | for i in range(Nlines): 174 | #bottom 175 | f = [0] 176 | for j in range(Nseg+1): 177 | f.append(i*(Nseg+1) + j + 1) 178 | faces.append(f) 179 | #top 180 | g = [fi + nVerts for fi in f[::-1]] 181 | faces.append(g) 182 | else: 183 | #star pattern 184 | for i in range(2*Nlines)[1::2]: 185 | #bottom 186 | fi1 = 0 187 | fi2 = 1 + (i+1)//2*Nseg + (i)//2 188 | fi3 = 1 + ((i+2)//2*Nseg + (i+1)//2)%(nVerts0-1) 189 | f = [fi1, fi2, fi3] 190 | faces.append(f) 191 | #top 192 | g = [fi + nVerts for fi in f[::-1]] 193 | faces.append(g) 194 | #outside ring 195 | for i in range(2*Nlines): 196 | #bottom 197 | f = [nVerts0 + (i*Nseg + j)%Nouter for j in range(Nseg+1)] 198 | if i%2 == 0: 199 | fi2 = 1 + (i+1)//2*Nseg + (i)//2 200 | fi3 = 1 + ((i+2)//2*Nseg + (i+1)//2)%(nVerts0-1) 201 | f = f + [x for x in np.arange(fi2,fi3+1)[::-1]] 202 | else: 203 | fi2 = 1 + (i+1)//2*Nseg + (i)//2 204 | fi3 = 1 + ((i+2)//2*Nseg + (i+1)//2)%(nVerts0-1) 205 | f = f + [fi3, fi2] 206 | faces.append(f) 207 | #top 208 | g = [fi + nVerts for fi in f[::-1]] 209 | faces.append(g) 210 | 211 | #faces of outer ring 212 | if not OE: 213 | for i in range(Nouter): 214 | fi1 = nVerts0 + (i+1)%(Nouter) 215 | fi2 = nVerts0 + i%(Nouter) 216 | fi3 = fi2 + nVerts 217 | fi4 = fi1 + nVerts 218 | faces.append([fi1,fi2,fi3,fi4]) 219 | 220 | 221 | mesh = bpy.data.meshes.new(name = 'OC_Siemens_Star') 222 | mesh.from_pydata(verts, edges, faces) 223 | obj = object_data_add(context, mesh, operator=self) 224 | 225 | -------------------------------------------------------------------------------- /bl_optics/sqlens.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | import bpy 23 | from bpy.types import Operator 24 | from bpy.props import FloatProperty, IntProperty, EnumProperty, StringProperty, BoolProperty, FloatVectorProperty 25 | from bpy_extras.object_utils import AddObjectHelper, object_data_add 26 | 27 | from .. import surface as sfc 28 | from .. import object_data_add 29 | from .. import utils 30 | 31 | class OBJECT_OT_add_sqlens(Operator, AddObjectHelper): 32 | """Create a new Mesh Object""" 33 | bl_idname = "mesh.add_sqlens" 34 | bl_label = "Square Lens" 35 | bl_options = {'REGISTER', 'UNDO'} 36 | 37 | cylindrical : BoolProperty( 38 | name="Cylinder Lens", 39 | default=False, 40 | ) 41 | ltype1 : EnumProperty( 42 | name="Surface 1 Type", 43 | items = {("spherical","Spherical",""), 44 | ("aspheric","Aspheric","")}, 45 | default = "spherical", 46 | description="Shape of Surface 1", 47 | #options={'HIDDEN'}, 48 | ) 49 | ltype2 : EnumProperty( 50 | name="Surface 2 Type", 51 | items = {("spherical","Spherical",""), 52 | ("aspheric","Aspheric","")}, 53 | default = "spherical", 54 | description="Shape of Surface 2", 55 | #options={'HIDDEN'}, 56 | ) 57 | rad1 : FloatProperty( 58 | name="Surface 1 Radius", 59 | default = 12., 60 | description="Radius of Curvature of Surface 1", 61 | unit = "LENGTH", 62 | ) 63 | rad2 : FloatProperty( 64 | name="Surface 2 Radius", 65 | default = 0, 66 | description="Radius of Curvature of Surface 2", 67 | unit = "LENGTH", 68 | ) 69 | num1 : IntProperty( 70 | name="N", 71 | default = 32, 72 | description="Number of vertices (per axis)", 73 | min=3, 74 | ) 75 | lenswidth : FloatProperty( 76 | name="Lens Width", 77 | default = 3., 78 | description="Width of the lens", 79 | unit = "LENGTH", 80 | ) 81 | centerthickness : FloatProperty( 82 | name="Center Thickness", 83 | default = 1., 84 | description="Center thickness of lens", 85 | unit = "LENGTH", 86 | ) 87 | k : FloatProperty( 88 | name="k", 89 | default = 0., 90 | description="Aspheric conical constant", 91 | ) 92 | A : FloatVectorProperty( 93 | name="A", 94 | default = (0.,0.,0.), 95 | description="Aspheric correction coefficients", 96 | ) 97 | k2 : FloatProperty( 98 | name="k2", 99 | default = 0., 100 | description="Aspheric conical constant", 101 | ) 102 | A2 : FloatVectorProperty( 103 | name="A2", 104 | default = (0.,0.,0.), 105 | description="Aspheric correction coefficients", 106 | ) 107 | material_name : StringProperty( 108 | name="Material", 109 | default="", 110 | ) 111 | shade_smooth : BoolProperty( 112 | name="Smooth Shading", 113 | default=True, 114 | ) 115 | smooth_type : BoolProperty( 116 | name="Use Custom Normals", 117 | default=True, 118 | ) 119 | ignore_surface_checks : BoolProperty( 120 | name="Ignore geometry checks", 121 | description="Ignores some geometric checks. Intermediate hotfix solution. MAY LEAD TO ERRORS!", 122 | default=False, 123 | ) 124 | 125 | def draw(self, context): 126 | layout = self.layout 127 | # Location 128 | col = layout.column(align=True) 129 | col.label(text="Location") 130 | col.prop(self, 'location', text="") 131 | # Rotation 132 | col = layout.column(align=True) 133 | col.label(text="Rotation") 134 | col.prop(self, 'rotation', text="") 135 | scene = context.scene 136 | layout.prop(self, 'ltype1') 137 | layout.prop(self, 'cylindrical') 138 | layout.prop(self, 'rad1') 139 | layout.prop(self, 'ltype2') 140 | layout.prop(self, 'rad2') 141 | layout.prop(self, 'lenswidth') 142 | layout.prop(self, 'centerthickness') 143 | layout.prop(self, 'num1') 144 | #layout.prop(self, 'num2') 145 | if self.ltype1=='aspheric': 146 | layout.prop(self, 'k') 147 | layout.prop(self, 'A') 148 | if self.ltype2=='aspheric': 149 | layout.prop(self, 'k2') 150 | layout.prop(self, 'A2') 151 | layout.prop_search(self, "material_name", bpy.data, "materials", icon="NONE") 152 | layout.prop(self, 'shade_smooth') 153 | if self.shade_smooth: 154 | layout.prop(self, 'smooth_type') 155 | layout.prop(self, 'ignore_surface_checks') 156 | 157 | def execute(self, context): 158 | add_sqlens(self, context) 159 | return {'FINISHED'} 160 | 161 | def add_sqlens(self, context): 162 | edges = [] 163 | 164 | srad1 = self.rad1 165 | srad2 = -self.rad2 166 | N1 = self.num1 167 | #N2 = self.num2 168 | N2 = N1 169 | lwidth = self.lenswidth 170 | CT = self.centerthickness 171 | if self.ltype1 == 'aspheric': 172 | k = self.k 173 | A = self.A 174 | if self.ltype2 == 'aspheric': 175 | k2 = self.k2 176 | A2 = self.A2 177 | 178 | ssig1 = 1 179 | if srad1 < 0: 180 | ssig1 = -1 181 | ssig2 = 1 182 | if srad2 < 0: 183 | ssig2 = -1 184 | 185 | if not self.ignore_surface_checks: 186 | #check surface radii for consistency 187 | ##check radius overflow 188 | if abs(srad1) < np.sqrt(2)/2*lwidth: srad1 = 0 189 | if abs(srad2) < np.sqrt(2)/2*lwidth: srad2 = 0 190 | ##check center thickness 191 | lsurf1, lsurf2 = 0, 0 192 | if not srad1 == 0: 193 | lsurf1 = srad1-ssig1*np.sqrt(srad1**2-0.5*lwidth**2) 194 | if not srad2 == 0: 195 | lsurf2 = srad2-ssig2*np.sqrt(srad2**2-0.5*lwidth**2) 196 | if (lsurf1 + lsurf2) > CT: 197 | CT = lsurf1 + lsurf2 198 | 199 | #add surface1 200 | if srad1 == 0: #flat surface case 201 | verts, faces, normals = sfc.add_sqflat_surface(lwidth,N1,N2) 202 | else: 203 | if self.ltype1 == 'spherical': 204 | verts, faces, normals = sfc.add_sqspherical_surface(srad1, lwidth, N1, N2, cylindrical=self.cylindrical) 205 | elif self.ltype1 == 'aspheric': 206 | verts, faces, normals = sfc.add_sqaspheric_surface(srad1, k, A, lwidth, N1, N2, cylindrical=self.cylindrical) 207 | 208 | nVerts = len(verts) 209 | 210 | #add side 211 | if srad1==0 and srad2 == 0: 212 | faces.append([1,2] + [nVerts+1, nVerts+2]) 213 | faces.append([2,3] + [nVerts, nVerts+1]) 214 | faces.append([3,0] + [nVerts+3, nVerts]) 215 | faces.append([0,1] + [nVerts+2, nVerts+3]) 216 | elif srad1!=0 and srad2==0: 217 | faces.append([i*N2 + N2-1 for i in range(N1)] + [nVerts+1, nVerts+2]) 218 | faces.append([i+N2*(N1-1) for i in range(N2)[::-1]] + [nVerts, nVerts+1]) 219 | faces.append([i*N2 for i in range(N1)[::-1]] + [nVerts+3, nVerts]) 220 | faces.append([i for i in range(N2)] + [nVerts+2, nVerts+3]) 221 | elif srad1==0 and srad2!=0: 222 | faces.append([1,2] + [i*N2 + 4 for i in range(N1)]) 223 | faces.append([2,3] + [i + 4 for i in range(N2)[::-1]]) 224 | faces.append([3,0] + [i*N2 + N2-1 + 4 for i in range(N1)[::-1]]) 225 | faces.append([0,1] + [i+N2*(N1-1) + 4 for i in range(N2)]) 226 | else: 227 | faces.append([i*N2 + N2-1 for i in range(N1)] + [i*N2 + nVerts for i in range(N1)]) 228 | faces.append([i+N2*(N1-1) for i in range(N2)[::-1]] + [i + nVerts for i in range(N2)[::-1]]) 229 | faces.append([i*N2 for i in range(N1)[::-1]] + [i*N2 + N2-1 + nVerts for i in range(N1)[::-1]]) 230 | faces.append([i for i in range(N2)] + [i+N2*(N1-1) + nVerts for i in range(N2)]) 231 | 232 | #add surface2 233 | if srad2 == 0: 234 | #flat surface case 235 | dvert, dfac, normals2 = sfc.add_sqflat_surface(lwidth,N1,N2,-1,CT,nVerts=nVerts) 236 | dvert = dvert[::-1] 237 | else: 238 | if self.ltype2 == 'spherical': 239 | dvert, dfac, normals2 = sfc.add_sqspherical_surface(srad2, lwidth, N1, N2, -1, CT, nVerts=nVerts, cylindrical=self.cylindrical) 240 | elif self.ltype2 == 'aspheric': 241 | dvert, dfac, normals2 = sfc.add_sqaspheric_surface(srad2, k2, A2, lwidth, N1, N2, -1, CT, nVerts=nVerts, cylindrical=self.cylindrical) 242 | #dvert, dfac = sfc.add_spherical_surface(srad2, lrad, N1, N2,-1, CT, nVerts=nVerts) 243 | dvert, dfac, normals2 = dvert[::-1], dfac[::-1], normals2[::-1] 244 | normals2 = [(-n[0], -n[1], -n[2]) for n in normals2] 245 | 246 | verts = verts+dvert 247 | faces = faces+dfac 248 | normals = normals + normals2 249 | 250 | del dvert 251 | del dfac 252 | 253 | mesh = bpy.data.meshes.new(name = 'OC_Square_Lens') 254 | mesh.from_pydata(verts, edges, faces) 255 | obj = object_data_add(context, mesh, operator=self) 256 | 257 | #assign material 258 | if self.material_name in bpy.data.materials: 259 | mat = bpy.data.materials[self.material_name] 260 | obj.data.materials.append(mat) 261 | 262 | 263 | if self.shade_smooth: 264 | bpy.ops.object.shade_smooth() 265 | if not self.smooth_type: 266 | pass 267 | else: 268 | #assign custom normals 269 | bpy.ops.mesh.customdata_custom_splitnormals_clear() 270 | bpy.ops.mesh.customdata_custom_splitnormals_add() 271 | cn1, cn2, cn3 = [], [], [] 272 | #surf1 273 | if srad1 == 0: 274 | nloops1 = 4 275 | nloopss1 = 1 276 | else: 277 | if N1%2 == 1: 278 | nloops1 = 3*2*(N1 - 1)**2 279 | else: 280 | nloops1 = 3*2*4*int((N1-1)/2)**2 + 4*(N1-1 + N1-2) 281 | nloopss1 = N1-1 282 | for i in range(nloops1): 283 | vi = mesh.loops[i].vertex_index 284 | cn1.append(normals[vi]) 285 | #surf2 286 | if srad2 == 0: 287 | nloops2 = 4 288 | nloopss2 = 1 289 | else: 290 | if N1%2 == 1: 291 | nloops2 = 3*2*(N1 - 1)**2 292 | else: 293 | nloops2 = 3*2*4*(int(N1/2) -1)**2 + 4*(N1-1 + N1-2) 294 | nloopss2 = N1-1 295 | for i in range(nloops2): 296 | vi = mesh.loops[i + nloops1 + 4*(nloopss1 + nloopss2 + 2)].vertex_index 297 | cn2.append(normals[vi]) 298 | #side 299 | cn3 = sfc.get_squarefacenormals(nloopss1, nloopss2) 300 | 301 | #execute 302 | mesh.normals_split_custom_set(cn1 + cn3 + cn2) 303 | 304 | #for testing 305 | #mesh.calc_normals_split() 306 | #bpy.ops.object.mode_set(mode='EDIT', toggle=False) 307 | #bpy.ops.mesh.select_all(action='DESELECT') -------------------------------------------------------------------------------- /bl_optomech/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | from .table import * 21 | from .post import * -------------------------------------------------------------------------------- /bl_optomech/lens_aperture.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from mathutils import Vector 23 | 24 | import bpy 25 | from bpy_extras.object_utils import object_data_add 26 | 27 | from ..bl_materials import add_blackoutmaterial_cycles, add_diffusematerial_cycles, add_blackoutmaterial_luxcore, add_diffusematerial_luxcore 28 | 29 | def add_circular_aperture(self, context, radius_inner, radius_outer, N, dshape=False): 30 | verts = [] 31 | edges = [] 32 | faces = [] 33 | 34 | maxb = 2*np.pi 35 | if dshape: 36 | maxb = np.pi*N/(N-1) 37 | 38 | # front face 39 | # create inner circle vertices 40 | for i in range(N): 41 | b = maxb*i/N 42 | verts.append(Vector((0, radius_inner*np.sin(b), radius_inner*np.cos(b)))) 43 | # create outer circle vertices 44 | for i in range(N): 45 | b = maxb*i/N 46 | verts.append(Vector((0, radius_outer*np.sin(b), radius_outer*np.cos(b)))) 47 | # back face 48 | # create inner circle vertices 49 | for i in range(N): 50 | b = maxb*i/N 51 | verts.append(Vector((-0.1, radius_inner*np.sin(b), radius_inner*np.cos(b)))) 52 | # create outer circle vertices 53 | for i in range(N): 54 | b = maxb*i/N 55 | verts.append(Vector((-0.1, radius_outer*np.sin(b), radius_outer*np.cos(b)))) 56 | 57 | # connect faces 58 | # front faces 59 | for i in range(N - dshape): 60 | fi1 = i 61 | fi2 = (i+1)%N 62 | fi3 = i + N 63 | fi4 = (i+1)%N + N 64 | faces.append([fi1, fi2, fi4, fi3]) 65 | # back faces 66 | for i in range(N - dshape): 67 | fi1 = i + 2*N 68 | fi2 = (i+1)%N + 2*N 69 | fi3 = i + N + 2*N 70 | fi4 = (i+1)%N + N + 2*N 71 | faces.append([fi3, fi4, fi2, fi1]) 72 | # side faces inner 73 | for i in range(N - dshape): 74 | fi1 = i 75 | fi2 = (i+1)%N 76 | fi3 = i + 2*N 77 | fi4 = (i+1)%N + 2*N 78 | faces.append([fi3, fi4, fi2, fi1]) 79 | # side faces outer 80 | for i in range(N - dshape): 81 | fi1 = i + N 82 | fi2 = (i+1)%N + N 83 | fi3 = i + N + 2*N 84 | fi4 = (i+1)%N + N + 2*N 85 | faces.append([fi1, fi2, fi4, fi3]) 86 | # d-shape 87 | if dshape: 88 | # top 89 | fi1 = 0 90 | fi2 = N 91 | fi3 = 2*N 92 | fi4 = 3*N 93 | faces.append([fi1, fi2, fi4, fi3]) 94 | # bottom 95 | fi1 = N - 1 96 | fi2 = 2*N - 1 97 | fi3 = 3*N - 1 98 | fi4 = 4*N - 1 99 | faces.append([fi3, fi4, fi2, fi1]) 100 | 101 | #create mesh from verts and faces 102 | mesh = bpy.data.meshes.new(name = 'OC_Aperture') 103 | mesh.from_pydata(verts, edges, faces) 104 | # useful for development when the mesh may be invalid. 105 | #mesh.validate(verbose=True) 106 | obj = object_data_add(context, mesh, operator=self) 107 | 108 | using_cycles = context.scene.render.engine == 'CYCLES' 109 | using_luxcore = context.scene.render.engine == 'LUXCORE' 110 | 111 | if using_cycles: 112 | materialname_aperture = add_blackoutmaterial_cycles(objectname='Aperture') 113 | if dshape: materialname_dface = add_diffusematerial_cycles(objectname='ApertureXSection') 114 | elif using_luxcore: 115 | materialname_aperture = add_blackoutmaterial_luxcore(objectname='Aperture') 116 | if dshape: materialname_dface = add_diffusematerial_luxcore(objectname='ApertureXSection') 117 | if using_cycles or using_luxcore: 118 | material_apertue = bpy.data.materials[materialname_aperture] 119 | ob = bpy.context.active_object 120 | ob.data.materials.append(material_apertue) 121 | if dshape: 122 | material_dface = bpy.data.materials[materialname_dface] 123 | ob.data.materials.append(material_dface) 124 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 125 | bpy.ops.mesh.select_all(action='DESELECT') 126 | sel_mode = bpy.context.tool_settings.mesh_select_mode 127 | bpy.context.tool_settings.mesh_select_mode = [False, False, True] # face select mode 128 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 129 | mesh.polygons[-1].select = True 130 | mesh.polygons[-2].select = True 131 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 132 | obj.active_material_index = 1 133 | bpy.ops.object.material_slot_assign() 134 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 135 | bpy.context.tool_settings.mesh_select_mode = [True, False, False] # reactivate vertex select mode -------------------------------------------------------------------------------- /bl_optomech/lens_housing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | import bpy 23 | from bpy_extras.object_utils import object_data_add 24 | 25 | from ..bl_materials import add_blackoutmaterial_cycles, add_blackoutmaterial_luxcore 26 | 27 | def add_lenshousing_simple(self, context, lens, verts_outline, dz_outline, 28 | CA_factor=0.99, housing_factor=1.1, thicksensor=False, sensorthickness=None, 29 | dshape=False, outlinetype='max'): 30 | """ 31 | This function creates a basic lens housing intended for quick raytracing setups. 32 | The basic logic is to create and aperture at every surface, scaled by CAfactor, 33 | and connecting them on the outside. 34 | Likewise, the sensor housing is modelled based on the sensor size and connected to the lens tube. 35 | """ 36 | if dshape: 37 | # Not currently implemented for cross section lenses 38 | return 39 | if outlinetype not in ['max', 'tight']: 40 | print(f"[OC] Warning: Invalid value for outlinetype in add_lenshousing_simple(): '{outlinetype}'. Defaulting to 'max'") 41 | outlinetype = 'max' 42 | 43 | verts = [] 44 | edges = [] 45 | faces = [] 46 | 47 | # create vertices at lenses and connect apertures 48 | offset = 0 49 | offset_list = [0] 50 | N_list = [] 51 | i_surf = 0 52 | for vo, dz in zip(verts_outline, dz_outline): 53 | v, squarelens = vo 54 | N_this = len(v) 55 | N_list.append(N_this) 56 | if i_surf%2 == 0: 57 | # TODO: this is not ideal and not a generally good solution 58 | # However, a better solution might need to re-evaluate each surface twice. And this approach may acutally be OK. 59 | # Reason: the tube outside the lenses is most important, slight leakage effect at the baffles could be acceptable 60 | # If anything, solidyfying the tube might be more important regarding leaks 61 | dz_epsilon = 0.1 62 | else: 63 | dz_epsilon = -0.1 64 | v2 = np.array(v) # create a copy 65 | if outlinetype == 'max': 66 | if squarelens: 67 | thisrad = abs(v2[0, 1]) 68 | else: 69 | thisrad = np.sqrt(np.sum(v2[0, 1:]*v2[0, 1:])) 70 | maxrad = np.nanmax(lens.data['lrad'][1:-1]) 71 | outline_factor = maxrad/thisrad 72 | else: 73 | outline_factor = 1 74 | v2[:,1:] = v2[:,1:]*CA_factor 75 | v2[:,0] = v2[:,0] + dz + dz_epsilon 76 | v2 = [[v2[i,0], v2[i,1], v2[i,2]] for i in range(N_this)] 77 | verts = verts + v2 78 | v2 = np.array(v) 79 | v2[:,1:] = v2[:,1:]*housing_factor*outline_factor 80 | v2[:,0] = v2[:,0] + dz + dz_epsilon 81 | v2 = [[v2[i,0], v2[i,1], v2[i,2]] for i in range(N_this)] 82 | verts = verts + v2 83 | for j in range(N_this): 84 | fi1 = offset + j 85 | fi2 = offset + (j+1)%N_this 86 | fi3 = offset + (j+1)%N_this + N_this 87 | fi4 = offset + j + N_this 88 | faces.append([fi4, fi3, fi2, fi1]) 89 | offset += 2*N_this 90 | offset_list.append(offset) 91 | i_surf += 1 92 | 93 | # save some numbers for interfacing with sensor box 94 | v2 = np.array(v2) 95 | rad_last = np.sqrt(np.max(np.sum(v2[:,1:]**2, axis=1))) 96 | nVerts_tube = len(verts) 97 | z_tube_last = v2[0,0] + dz_epsilon # add dz_epsilon to avoid issues with bridge_edge_loops 98 | 99 | # connect lens tube 100 | for i in range(1, len(verts_outline)): 101 | if not N_list[i] == N_list[i-1]: 102 | # unequal sides connected later using bridge_edge_loops 103 | continue 104 | N_this = N_list[i] 105 | offset0 = offset_list[i-1] + N_this # index offset to the previous aperture incl. inner ring 106 | offset1 = offset_list[i] + N_this # index offset to this aperture incl. inner ring 107 | for j in range(N_this): 108 | fi1 = offset0 + j 109 | fi2 = offset0 + (j+1)%N_this 110 | fi3 = offset1 + (j+1)%N_this 111 | fi4 = offset1 + j 112 | faces.append([fi4, fi3, fi2, fi1]) 113 | 114 | # create box around sensor 115 | length_lensbarrel = max(dz_outline) - min(dz_outline) 116 | # get sensor position and size 117 | # x/y extent 118 | len_sensor = max(lens.detector['sizex'], lens.detector['sizex']) 119 | len_sh = max(len_sensor/2, rad_last)*1.1 120 | # z-position. Assumption: Sersor is perpendicular to z-axis 121 | det_quad = np.array(lens.detector['Quad']) 122 | det_z = det_quad[0][2] 123 | if thicksensor: 124 | z_sh_back = -det_z - 2*sensorthickness 125 | else: 126 | z_sh_back = -det_z - 0.05*length_lensbarrel 127 | verts.append([z_tube_last, -len_sh, -len_sh]) 128 | verts.append([z_tube_last, len_sh, -len_sh]) 129 | verts.append([z_tube_last, len_sh, len_sh]) 130 | verts.append([z_tube_last, -len_sh, len_sh]) 131 | verts.append([z_sh_back, -len_sh, -len_sh]) 132 | verts.append([z_sh_back, len_sh, -len_sh]) 133 | verts.append([z_sh_back, len_sh, len_sh]) 134 | verts.append([z_sh_back, -len_sh, len_sh]) 135 | o = nVerts_tube # just for keeping the following lines short 136 | faces.append([o, o+1, o+5, o+4][::-1]) # sides 137 | faces.append([o+1, o+2, o+6, o+5][::-1]) 138 | faces.append([o+2, o+3, o+7, o+6][::-1]) 139 | faces.append([o+3, o, o+4, o+7][::-1]) 140 | faces.append([o+4, o+5, o+6, o+7][::-1]) # rear wall 141 | 142 | # create object 143 | mesh = bpy.data.meshes.new(name = 'OC_LensHousing') 144 | mesh.from_pydata(verts, edges, faces) 145 | obj = object_data_add(context, mesh, operator=self) 146 | 147 | # bridge unconnected 148 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 149 | bpy.ops.mesh.select_all(action='DESELECT') 150 | bpy.context.tool_settings.mesh_select_mode = [True, False, False] # Vertex select mode 151 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 152 | for i in range(1, len(verts_outline)): 153 | if N_list[i] == N_list[i-1]: 154 | # opposite case as in step "connect lens tube" 155 | continue 156 | N_before = N_list[i-1] 157 | N_this = N_list[i] 158 | offset0 = offset_list[i-1] + N_before # index offset to the previous aperture incl. inner ring 159 | offset1 = offset_list[i] + N_this # index offset to this aperture incl. inner ring 160 | for j in range(N_before): 161 | fi1 = offset0 + j 162 | mesh.vertices[fi1].select=True 163 | for j in range(N_this): 164 | fi3 = offset1 + (j+1)%N_this 165 | mesh.vertices[fi3].select=True 166 | 167 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 168 | bpy.ops.mesh.bridge_edge_loops() 169 | bpy.ops.mesh.select_all(action='DESELECT') 170 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 171 | 172 | mesh = obj.data # get the updated mesh 173 | 174 | # create interface between sensor box and lens tube 175 | i = len(verts_outline) - 1 # should be preserved from above but bettzer be explicit 176 | N_this = N_list[i] 177 | offset1 = offset_list[i] + N_this # index offset to this aperture incl. inner ring 178 | for j in range(N_this): 179 | fi3 = offset1 + (j+1)%N_this 180 | mesh.vertices[fi3].select=True 181 | o = nVerts_tube 182 | for j in range(4): 183 | ei1 = o + j 184 | mesh.vertices[ei1].select=True 185 | 186 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 187 | bpy.ops.mesh.bridge_edge_loops() 188 | bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 189 | 190 | # assign material 191 | using_cycles = context.scene.render.engine == 'CYCLES' 192 | using_luxcore = context.scene.render.engine == 'LUXCORE' 193 | 194 | if using_cycles: 195 | materialname_aperture = add_blackoutmaterial_cycles(objectname='Housing') 196 | elif using_luxcore: 197 | materialname_aperture = add_blackoutmaterial_luxcore(objectname='Housing') 198 | if using_cycles or using_luxcore: 199 | material_apertue = bpy.data.materials[materialname_aperture] 200 | ob = bpy.context.active_object 201 | ob.data.materials.append(material_apertue) -------------------------------------------------------------------------------- /bl_raytrace/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/bl_raytrace/__init__.py -------------------------------------------------------------------------------- /bl_raytrace/trace_scene.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | import bpy 23 | 24 | def trace_to_scene(context, rays): 25 | EPSILON = 0.001 26 | P = [] 27 | anyhit = False 28 | O, D = rays.get_rays() 29 | for i in range(O.shape[0]): 30 | o = np.array(O[i,:]) 31 | d = np.array(D[i,:]) 32 | o = o + d*EPSILON 33 | o[[0,1,2]] = o[[2,0,1]] 34 | d[[0,1,2]] = d[[2,0,1]] 35 | o[0] = o[0]*-1 36 | d[0] = d[0]*-1 37 | scene = context.scene 38 | graph = bpy.context.evaluated_depsgraph_get() 39 | result = scene.ray_cast(graph, origin=o, direction = d) 40 | hit, p, normal, index, ob, matrix = result 41 | p = np.array(p) 42 | p[[2,0,1]] = p[[0,1,2]] 43 | if hit: 44 | anyhit = True 45 | p[2] = p[2]*-1 46 | P.append(p) 47 | else: 48 | P.append([float('nan'), float('nan'), float('nan')]) 49 | P = np.array(P) 50 | idx_fail = np.isnan(P[:,0]) 51 | rays.update(P, None, None, None, idx_fail) 52 | 53 | return rays -------------------------------------------------------------------------------- /bl_scenes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/bl_scenes/__init__.py -------------------------------------------------------------------------------- /bl_scenes/luxcore_lights.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | import bpy 20 | 21 | import numpy as np 22 | 23 | from ..utils.geometry import get_rotmat_x, get_rotmat_z 24 | 25 | def add_laser(location=(0,0,0), rotation=(0,0,0), 26 | lasersize=0.1): 27 | lightname = 'OC_Laser' 28 | light_data = bpy.data.lights.new(name=lightname, type='AREA') 29 | light_object = bpy.data.objects.new(name=lightname, object_data=light_data) 30 | bpy.context.collection.objects.link(light_object) 31 | 32 | light_object.data.luxcore.is_laser = True 33 | light_object.data.size = lasersize 34 | 35 | light_object.location = location 36 | 37 | # rotation is handled in YZX mode 38 | # Y should be 90 degree to align along the negative x-axis 39 | # Then Z describes the FOV angle 40 | # and X finally the azimuth 41 | light_object.rotation_mode = 'YZX' 42 | light_object.rotation_euler = rotation 43 | 44 | return light_object 45 | 46 | 47 | def add_laser_array(HFOV=0, VFOV=0, numH=4, numV=4, 48 | distance=20, 49 | rp=[0,0,0], lasersize=0.1): 50 | created_lights = [] 51 | if HFOV == 0: 52 | HFOV_list = [0] 53 | else: 54 | HFOV_list = np.linspace(-HFOV, HFOV, 2*numH+1, endpoint=True)*np.pi/180 55 | if VFOV == 0: 56 | VFOV_list = [0] 57 | else: 58 | VFOV_list = np.linspace(-VFOV, VFOV, 2*numV+1, endpoint=True)*np.pi/180 59 | 60 | for H in HFOV_list: 61 | for V in VFOV_list: 62 | # the following calculation is based on the assumption of 63 | # h = f*tan(theta), but 64 | # since f will cancel it is ommited here (or equivalently, assumed f=1) 65 | # step 1: determine rotation 66 | hx = np.tan(H) 67 | hy = np.tan(V) 68 | theta = np.arctan(np.sqrt(hx*hx + hy*hy)) 69 | phi = np.arctan2(hy, hx) 70 | rotation = [phi, np.pi/2, theta] 71 | # step 2, rotate location around rp (rotation point) 72 | rp = np.array(rp) 73 | location = np.array([distance, 0, 0]) - rp # base location 74 | RZ = get_rotmat_z(theta) 75 | RX = get_rotmat_x(phi) 76 | location = np.matmul(RZ, location) 77 | location = np.matmul(RX, location) 78 | location = location + rp 79 | obj = add_laser(location, rotation, lasersize=lasersize) 80 | created_lights.append(obj.name) 81 | 82 | return created_lights -------------------------------------------------------------------------------- /bl_systems/__init__.py: -------------------------------------------------------------------------------- 1 | from .load_zmx import * 2 | # from .test_zemax_library import * -------------------------------------------------------------------------------- /bl_systems/test_zemax_library.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | import bpy 5 | from bpy_extras.object_utils import AddObjectHelper 6 | 7 | from . import load_zmx 8 | 9 | 10 | """ 11 | THIS FILE IS NOT COMMITTED 12 | FUNCTIONS MUST BE DIABLED IN MASTER __init__.py 13 | BEFORE PUSHING A COMMIT! 14 | """ 15 | 16 | 17 | basefolder = r'C:\Users\johannes\Documents\0_optics\lensdesigns_com' 18 | design_types = ['photo_prime', 'photo_zoom', 'telescope', 'eyepiece', 'microscope', 'projector'] 19 | 20 | class OBJECT_OT_test_zmx(bpy.types.Operator, AddObjectHelper): 21 | """Create a new Mesh Object""" 22 | bl_idname = "mesh.test_zmx" 23 | bl_label = "Test zmx files" 24 | bl_options = {'REGISTER', 'UNDO'} 25 | 26 | def draw(self, context): 27 | pass 28 | 29 | def execute(self, context): 30 | test_zemax_library() 31 | postprocess_valid_lenses() 32 | return {'FINISHED'} 33 | 34 | def test_zemax_library(): 35 | for design_type in design_types: 36 | folder = os.path.join(basefolder, design_type) 37 | logfile = os.path.join(basefolder, 'log_' + design_type + '.txt') 38 | f_log = open(logfile, 'a') 39 | 40 | design_files = next(os.walk(folder))[2] 41 | zmx_files = [] 42 | for f in design_files: 43 | if 'nonseq' in f.lower(): 44 | continue 45 | if f.lower().endswith('.zmx'): zmx_files.append(f) 46 | 47 | for f in zmx_files: 48 | print() 49 | print('NOW: ', f) 50 | fname = os.path.join(folder, f) 51 | f_log.write(f) 52 | try: 53 | lens = load_zmx.load_from_zmx(fname, testmode=True, logfile=f_log) 54 | except: 55 | f_log.write(' FAILED') 56 | f_log.write('\n') 57 | 58 | f_log.close() 59 | 60 | def postprocess_valid_lenses(): 61 | usablefile = os.path.join(basefolder, 'functional_lenses.txt') 62 | f_use = open(usablefile, 'a') 63 | glassfile = os.path.join(basefolder, 'missing_glasses.txt') 64 | f_glas = open(glassfile, 'a') 65 | glassfile2 = os.path.join(basefolder, 'missing_glasses_rii.txt') 66 | f_glas2 = open(glassfile2, 'a') 67 | riifile = r'C:\Users\johannes\Documents\software\py\py_simpletrace\data\refractiveindexinfo\database\data-nk\rii_mapping.dat' 68 | f_rii = open(riifile) 69 | glasses_rii = [] 70 | glasses = [] 71 | for line in f_rii.readlines(): 72 | glasscode = line.split()[0] 73 | glasses_rii.append(glasscode.lower()) 74 | f_rii.close() 75 | for design_type in design_types: 76 | f_use.write(design_type + '\n') 77 | f_use.write('----------------------------------------------\n') 78 | logfile = os.path.join(basefolder, 'log_' + design_type + '.txt') 79 | f_log = open(logfile, 'r') 80 | for line in f_log.readlines(): 81 | n = len(line.split()) 82 | if 'FAILED' in line: 83 | continue 84 | elif n==1: 85 | f_use.write(line) 86 | else: 87 | glasslist = line.split()[1:] 88 | for g in glasslist: 89 | if g == 'FAILED': 90 | pass 91 | elif not g in glasses: 92 | glasses.append(g) 93 | f_use.write('\n') 94 | f_log.close() 95 | f_use.close() 96 | glasses = sorted(glasses) 97 | l1 = glasses[0][0] 98 | for g in glasses: 99 | l2 = g[0] 100 | if l2 != l1: 101 | f_glas.write('\n') 102 | f_glas2.write('\n') 103 | l1 = l2 104 | f_glas.write(g + ' ') 105 | if g.lower() not in glasses_rii: 106 | f_glas2.write(g + ' ') 107 | 108 | 109 | f_glas.close() 110 | f_glas2.close() 111 | 112 | if __name__ == '__main__': 113 | #test_zemax_library() 114 | postprocess_valid_lenses() 115 | 116 | -------------------------------------------------------------------------------- /changes.txt: -------------------------------------------------------------------------------- 1 | v1.1 2 | - Added cylindircal option to square lens 3 | - Added aspheric option to square lens 4 | - Fixed a label in square lens 5 | - Removed smooth shading option from ccretro 6 | - Fully mathematical custom normals for all lens and mirror variant, now default setting 7 | - added focal length info field, with IOR input field that is not connected to the rendering material! (Impossible in general because of textures) 8 | 9 | v1.0 10 | - First Version -------------------------------------------------------------------------------- /fileparser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/fileparser/__init__.py -------------------------------------------------------------------------------- /fileparser/zmx/__init__.py: -------------------------------------------------------------------------------- 1 | from .import_zmx import * 2 | 3 | -------------------------------------------------------------------------------- /fileparser/zmx/surfaces.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from .zmx2oc import determine_OC_surftype 23 | from ...utils.check_surface import surftype_zmx2ltype 24 | 25 | SUPPORTED_SURFTYPES =['STANDARD', 'EVENASPH', 'BICONICX', 'TOROIDAL'] 26 | 27 | 28 | def _type_from_surflines(surflines): 29 | surftype = 'STANDARD' # default, as zemax file does not need to contain TYPE line (seen in edmund optics doublets) 30 | for line in surflines: 31 | # Get the tr 32 | if line.startswith('TYPE'): 33 | surftype = line.split()[1] 34 | # had some files where XASPHERE was used in addition to EVENASPH 35 | # It seems that in those cases, EVENASPH only goes up to 8th-order coefficients and XASPHERE also to higher ones. Otehr differences I did not find reference to online. 36 | # For that reason, treating them as the same here. 37 | if surftype == 'XASPHERE': surftype = 'EVENASPH' 38 | return surftype 39 | 40 | def parse_zmx_surface(surflines): 41 | # This function returns a dictionary containing the interpreted parameters 42 | # converted to the OptiCore interpretation. 43 | # surf_info is filled i subroutines and combined at the end of this function. 44 | surf_info = {} 45 | 46 | # defaults if parameters not given for this surface 47 | surftype = None 48 | isstop = False 49 | radiusX = None 50 | kX = None 51 | AX = [] 52 | radiusY = None 53 | kY = None 54 | AY = [] 55 | CT = None 56 | hasglass = False 57 | ismirror = False 58 | outline_shape = 'circular' 59 | glass = None 60 | rCA = None 61 | rCA_short = None 62 | lrad = None 63 | coating = ['FRESNEL_0', None, None] 64 | 65 | # get the surface type first in order to know how to interpret the rest 66 | surftype = _type_from_surflines(surflines) 67 | if not surftype in SUPPORTED_SURFTYPES: 68 | raise ValueError(f'ERROR: Zemax surface type "{surftype}" not implemented in OptiCore. Please open a support ticket on GitHub and include a .zmx file ') 69 | 70 | # search the parameters 71 | for line in surflines: 72 | # surftype-independent parameters 73 | if line.startswith('STOP'): 74 | isstop = True 75 | elif line.startswith('CURV'): 76 | curv = float(line.split()[1]) 77 | radiusX = 0 if curv == 0 else 1./curv 78 | elif line.startswith('DISZ'): 79 | CT = float(line.split()[1]) 80 | elif line.startswith('GLAS'): 81 | hasglass = True 82 | glass = line.split()[1] 83 | if glass == '___BLANK': 84 | glass = ' '.join(line.split()[1:]) 85 | if glass == 'MIRROR': 86 | ismirror = True 87 | hasglass = False 88 | elif line.startswith('DIAM'): 89 | rCA = float(line.split()[1]) 90 | elif line.startswith('MEMA'): 91 | lrad = float(line.split()[1]) 92 | elif line.startswith('SQAP'): 93 | outline_shape = 'square' 94 | rCA_short = float(line.split()[1]) 95 | rCA = float(line.split()[2]) 96 | elif line.startswith('CONI'): 97 | kX = float(line.split()[1]) 98 | elif line.startswith('OCCT'): 99 | coat_type = line.split()[1] 100 | coat_filename = line.split()[2] 101 | coat_idx = int(line.split()[3]) 102 | coating = [coat_type, coat_filename, coat_idx] 103 | 104 | # surftype-dependent parameters 105 | elif line.startswith('PARM'): 106 | iparm = int(line.split()[1]) 107 | if surftype == 'EVENASPH': 108 | if iparm == 1: 109 | # I have yet to see a file that has a power-2 coefficient 110 | # (assuming iparm == 1 is for power 2 coefficient) 111 | # The OptiCore-geometry function currently starts at power-4, therefore ignoring this for the moment. 112 | # TODO: Add power-2 and refactor rest of code to account for this 113 | pass 114 | else: 115 | AX.append(float(line.split()[2])) 116 | elif surftype == 'BICONICX': 117 | if iparm == 1: 118 | radiusY = float(line.split()[2]) 119 | elif surftype == 'TOROIDAL': 120 | surf_rotation = float(line.split()[2]) 121 | elif line.startswith('XDAT'): 122 | iparm = int(line.split()[1]) 123 | if surftype == 'TOROIDAL' and iparm == 2: 124 | radiusY = float(line.split()[2]) 125 | 126 | # determine the surface type in the OptiCore scheme 127 | surftype = determine_OC_surftype(radiusX, kX, AX, radiusY, kY, AY) 128 | 129 | # enter default values into undefined variables 130 | if AX == []: AX = [None] 131 | if AY == []: AY = [None] 132 | if rCA_short is None: rCA_short = rCA 133 | 134 | # determine the lenstype for the Lens() class and match conventions 135 | ltype = surftype_zmx2ltype(radiusX, radiusY, kX, kY, AX, AY) 136 | if ltype == 'cylindricX': 137 | ltype = 'cylindrical' 138 | surf_rotation = np.pi/2 139 | elif ltype == 'cylindricY': 140 | radiusX, kX, AX, radiusY, kY, AY = radiusY, kY, AY, radiusX, kX, AX 141 | ltype = 'cylindrical' 142 | surf_rotation = 0 143 | elif ltype == 'toric': 144 | surf_rotation = 0#np.pi/2 145 | else: 146 | surf_rotation = 0 147 | 148 | # fill the return dictionary 149 | surf_info['type'] = surftype # 'type' is teh OptiCore surftype, i.e. base shape types w/o direction 150 | surf_info['ltype'] = ltype # ltype is for the Lens class, may include directions, like cylindricalX 151 | surf_info['isstop'] = isstop 152 | surf_info['radius'] = radiusX 153 | surf_info['asph'] = [kX] + AX 154 | surf_info['radius2'] = radiusY 155 | surf_info['asph2'] = [kY] + AY 156 | surf_info['surf_rotation'] = surf_rotation 157 | #surf_info['radiusX'] = radiusX 158 | #surf_info['asphX'] = [kX] + AX 159 | surf_info['CT'] = CT 160 | surf_info['hasglass'] = hasglass 161 | surf_info['ismirror'] = ismirror 162 | surf_info['glass'] = glass 163 | surf_info['rCA'] = rCA 164 | surf_info['rCA_short'] = rCA_short 165 | surf_info['lrad'] = lrad 166 | surf_info['outline_shape'] = outline_shape 167 | surf_info['coating'] = coating 168 | return surf_info 169 | 170 | def get_stop(surf_infos, idx_first=0): 171 | stopidx = 1 172 | for idx, surf in surf_infos.items(): # Use first surface as default stop location 173 | if idx == 1: 174 | if surf['rCA'] is not None: 175 | stoprad = surf['rCA'] 176 | elif surf['lrad'] is not None: 177 | stoprad = surf['lrad'] 178 | break 179 | for idx, surf in surf_infos.items(): # override stop where 'isstop' is set 180 | if surf['isstop']: 181 | stopidx = idx 182 | if surf['rCA'] is not None: 183 | stoprad = surf['rCA'] 184 | elif surf['lrad'] is not None: 185 | stoprad = surf['lrad'] 186 | if idx_first > stopidx: # Aperture in front of first glass surface 187 | CT_list = [surf_infos[i]['CT'] for i in range(stopidx, idx_first)] 188 | z_stop = -sum(CT_list) 189 | else: 190 | CT_list = [surf_infos[i]['CT'] for i in range(idx_first, stopidx)] 191 | z_stop = sum(CT_list) 192 | return stopidx, stoprad, z_stop -------------------------------------------------------------------------------- /fileparser/zmx/zmx2oc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def determine_OC_surftype(radX, kX, AX, radY=None, kY=None, AY=None): 23 | """ 24 | This function takes surface coefficients 25 | and determines the corresponding surface shape type for further processing. 26 | The result does not depend on the order of X- and Y-coefficients, but either input is allowed. 27 | """ 28 | # first, check that either any radius or A coefficients are defined, else return undefined 29 | noRadX = radX is None 30 | noRadY = radY is None 31 | noConicX = kX is None 32 | noConicY = kY is None 33 | noPolyX = np.all(np.array(AX) == None) or AX == [] or AX is None 34 | noPolyY = np.all(np.array(AY) == None) or AY == [] or AY is None 35 | XisNone = noRadX and noPolyX 36 | YisNone = noRadY and noPolyY 37 | 38 | # Initial check that anything was passed at all 39 | if XisNone and YisNone: 40 | return 'UNDEFINED' 41 | 42 | # In case Y was supplied and X not, flip around. 43 | # This will simplify case evaluation below 44 | # because XisNone does not have to be rechecked 45 | if XisNone: 46 | noRadX, noConicX, noPolyX, XisNone, noRadY, noConicY, noPolyY, YisNone = noRadY, noConicY, noPolyY, YisNone, noRadX, noConicX, noPolyX, XisNone 47 | radX, kX, AX, radY, kY, AY = radY, kY, AY, radX, kX, AX 48 | 49 | hasradX = radX != 0 and radX is not None 50 | hasradY = radY != 0 and radY is not None 51 | hasconicX = kX != 0 and kX is not None 52 | hasconicY = kY != 0 and kY is not None 53 | haspolyX = not (np.all(np.array(AX) == 0) or np.all(np.array(AX) == None) or AX is None) 54 | haspolyY = not (np.all(np.array(AY) == 0) or np.all(np.array(AY) == None) or AY is None) 55 | 56 | """ 57 | determine flat, rotational, cylindric, or toric case, 58 | and then subdivide further depending ona vailable coefficients 59 | flat: flat definition for one or both directions (radius == 0 and no aspheric coefficients) 60 | rotational: definition given only for one direction, rest is None 61 | cylindric: any non-flat definition for one direction, flat for the other 62 | toric: non-flat definition for both 63 | TODO: freeform 64 | 65 | return types of the above categories: 66 | Flat options: flat, 67 | rotational options: spherical, conical, polynominal, aspherical, 68 | cylindrical options: cylindrical, conicylindrical, polycylindrical, acylindrical, 69 | toric options: toric, conitoric, polytoric, atoric 70 | """ 71 | 72 | # first test for flat 73 | XisFlat = radX == 0 and not haspolyX 74 | YisFlat = radY == 0 and not haspolyY 75 | Surfisflat = (XisFlat and YisFlat) or (XisFlat and YisNone) 76 | if Surfisflat: 77 | return 'flat' 78 | 79 | # second test for rotational 80 | # remember: due to intial check-and-flip, XisNone == False is guaranteed here 81 | IsRotational = YisNone 82 | if IsRotational: 83 | if (hasradX and not hasconicX and not haspolyX): 84 | return 'spherical' 85 | elif (hasradX and hasconicX and not haspolyX): 86 | return 'conical' 87 | elif (not hasradX and haspolyX): 88 | return 'polynominal' 89 | else: 90 | return 'aspheric' 91 | 92 | # third test for cylindric 93 | isCylindric = YisFlat or (not YisNone and XisFlat) 94 | # remember: hasradX == True implies XisFlat == False 95 | if isCylindric: 96 | if (hasradX and not hasconicX and not haspolyX) or (hasradY and not hasconicY and not haspolyY): 97 | return 'cylindrical' 98 | elif (hasradX and hasconicX and not haspolyX) or (hasradY and hasconicY and not haspolyY): 99 | return 'conicylindrical' 100 | elif (not hasradX and haspolyX) or (not hasradY and haspolyY): 101 | return 'polycylindrical' 102 | else: 103 | return 'acylindrical' 104 | 105 | # fourth test for toric 106 | isToric = (not XisFlat) and (not YisNone and not YisFlat) 107 | if isToric: 108 | if (hasradX and not hasconicX and not haspolyX) and (hasradY and not hasconicY and not haspolyY): 109 | return 'toric' 110 | elif (hasradX and hasconicX and not haspolyX) and (hasradY and hasconicY and not haspolyY): 111 | return 'conitoric' 112 | elif (not hasradX and haspolyX) and (not hasradY and haspolyY): 113 | return 'polytoric' 114 | else: 115 | # currently this covers mixed cases. Are they relevant though, or would that be freeform anyways? 116 | return 'atoric' 117 | 118 | # Final case (UNDEFINED) that something odd slipped past the above tests 119 | print('ERROR in function "determine_OC_surftype". Coefficients do not match any implemented surface shape') 120 | return 'UNDEFINED' -------------------------------------------------------------------------------- /raytrace/coatingdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | """ 21 | In the Element and Lenssystem classes, coating are stored for each surface as a 3-element list. Items are in order: 22 | 1) type: Can be 'MIRROR' for ideal mirror, 'DATA' for tabulated AR-coating-data, or None, then Fresnel-reflectance will be computed 23 | 2) datasource: filename data source. data must be in column form, column 0 is wavelength in nm, further columns are reflectances in percent 24 | 3) dataindex: column index for the data in the file 25 | """ 26 | 27 | import numpy as np 28 | 29 | class CoatingData(): 30 | def __init__(self, type): 31 | self.type = type 32 | self.wl_data = None 33 | self.reflectance_data = None 34 | self.filename = None 35 | 36 | def load_coating_csv(self, filename): 37 | self.filename = filename 38 | # input must be: 39 | # column 1: wavelength in nm 40 | # other columns: refelctance in percent 41 | data = np.loadtxt(filename, delimiter=',') 42 | self.wl_data = data[:,0]/1000 # convert to um to follow convention in rest of OptiCore 43 | self.reflectance_data = data[:, 1:]/100 # convert from percent to abs 44 | if len(self.reflectance_data.shape) == 1: 45 | self.reflectance_data = self.reflectance_data[:, np.newaxis] 46 | 47 | def get_R(self, *params): 48 | # get reflectance 49 | if self.type == 'DATA': 50 | return self.get_reflectance_data(params[0], params[1]) 51 | elif self.type == 'MIRROR': 52 | return 1 53 | elif self.type == 'FRESNEL_0': 54 | return self.get_R_fresnel_0(params[0]) 55 | elif self.type == 'FIXVALUE': 56 | return self.reflectance_data 57 | else: 58 | return self.get_R_fresnel_0(params[0]) 59 | 60 | 61 | def get_reflectance_data(self, wl_um, index): 62 | # wl outside data range 63 | if wl_um <= self.wl_data[0]: 64 | return self.reflectance_data[0, index] 65 | elif wl_um >= self.wl_data[-1]: 66 | return self.reflectance_data[-1, index] 67 | 68 | # regular case 69 | idx_min = np.where(self.wl_data < wl_um)[0][-1] 70 | idx_max = np.where(self.wl_data > wl_um)[0][0] 71 | wl_min = self.wl_data[idx_min] 72 | wl_max = self.wl_data[idx_max] 73 | r_min = self.reflectance_data[idx_min, index] 74 | r_max = self.reflectance_data[idx_max, index] 75 | 76 | return r_min + (wl_um - wl_min)/(wl_max - wl_min)*(r_max - r_min) 77 | 78 | def get_R_fresnel_0(self, n_ratio, theta=0): 79 | # fresnel reflection for unpolarized ligth at 0 degree incidence 80 | if n_ratio < 1: n_ratio = 1./n_ratio 81 | 82 | R_root = (n_ratio - 1)/(n_ratio + 1) 83 | 84 | return R_root**2 85 | 86 | # TODO: implement numpy-compatible evaluation including angle -------------------------------------------------------------------------------- /raytrace/element.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | """ 21 | This file implements an optical Element class. 22 | An Element is equivalent to what is usally called a group in a lens system, i.e. a singlet, a cemented doublet, etc. 23 | Another example is a single mirror in a reflective telescope. 24 | 25 | The surfaces in an Element are defined relative to the optical axis of the first surface. 26 | In other words, the Element is defined in its local coordinates and does not store information about global 3D-positon and orientation. 27 | 28 | The separate class Lenssystem forms a complete system based on a list of Elements and their geometric arrangement. 29 | """ 30 | 31 | ALLOWED_SURFTYPES = ['flat', # flat shapes 32 | 'spherical', 'conical', 'polynominal', 'aspheric', # axial symmetry 33 | 'cylindrical', 'conicylindrical', 'polycylindrical', 'acylindrical', # cylinder symmtry 34 | 'toric', 'conitoric', 'polytoric', 'atoric'] # toric shape 35 | 36 | class Element(): 37 | def __init__(self, name='NONAME'): 38 | self.name = name 39 | self.data = {} 40 | 41 | """lists with length n_surfaces""" 42 | ## surface geometry - optical 43 | self.data['type'] = [] # Surface type for specific functions 44 | self.data['lenstype'] = [] # For the OptiCore Lens class, stores its shape class, e.g. cylindricalX 45 | self.data['radius'] = [] # Spherical surface radius [r0, r1, ...]. 46 | self.data['asph'] = [] # Apsheric coefficients [[k, a4, a6,...], ...]. 47 | self.data['radius2'] = [] # secondary radius list for toric lenses 48 | self.data['asph2'] = [] # secondary aspheric list for toric lenses 49 | # self.data['freeform_coeff'] = [] # coefficients for freeform-equation 50 | 51 | ## surface geometry - mechanical 52 | self.data['lrad'] = [] # Mechanical Lens radius. Radius for round lenses, side half-length for square lens. List of two half-lengths for rectangular lenses 53 | self.data['rCA'] = [] # Radius of optically ground surface. Same logic as for lrad. Must be smaller or equal to lrad. Difference will be created as flat annulus 54 | self.data['rCA_short'] = [] # Short axis of rCA, used in case of e.g. cylinder lens 55 | self.data['surf_rotation'] = [] # angle by which surface is rotated around optical axis. E.g. for cylinder lenses. 56 | 57 | ## imperfections 58 | self.data['surf_decenter'] = [] # Surface lateral position error [x, y] in local coordinates 59 | self.data['surf_tilt'] = [] # Surface tilt error [X, Y] in local coordinates 60 | 61 | ## other surface properties 62 | self.data['coating'] = [] # coatings [type, datasource, dataindex] 63 | # self.data['roughness_model'] = [] # for scattering 64 | # self.data['roughness_coeffs'] 65 | 66 | # """lists with length n_surfaces - 1""" 67 | self.data['CT'] = [] # Center thickness 68 | self.data['material'] = [] # Glass type specifiers 69 | 70 | # TEMPLATE 71 | # self.data[''] = [] 72 | 73 | # global element parameters 74 | self.outline_shape = 'circular' # alternative options: square, rectangular 75 | 76 | # status flags 77 | self.ismirror = False # This is specifically in case of a single, discrete mirror with one surface 78 | self.isdummy = False # dummy lenses for placing e.g. an aperture 79 | self.isreverse = False # indicates if the element has been reversed from its original state 80 | self.direction = 1 # indicates to the Blender object generation in which direction the element is facing 81 | # self.orientation = [0, 0, 0] # angles in spherical coordiantes [latitude, longitude, axis roll] to specify how this Element shall be oriented in 3D-space. Order of application: 1. Roll aorund z-axis, 2. latitude around x-axis, 3. longitude around y-axis. 82 | 83 | def add_surface(self, surftype, lenstype='rotational', radius=None, asph=[None, None], radius2=None, asph2=[None, None], 84 | coating=[None, None, None], rCA=None, rCA_short = None, lrad=None, CT=None, material=None, 85 | surf_decenter=None, surf_tilt=None, surf_rotation=0): 86 | """ 87 | function to append appropriately to the data structure 88 | """ 89 | if not surftype in ALLOWED_SURFTYPES: 90 | raise ValueError('ERROR: Element.add_surface received unknown surftype:', surftype) 91 | 92 | if len(self.data['radius']) != len(self.data['CT']): 93 | raise ValueError('ERROR in Element.add_surface: Mismatch of data structure length. Possibly trying to append to completed element?') 94 | 95 | if rCA_short is None: rCA_short = rCA 96 | 97 | # parameters independent of surftype 98 | self.data['type'].append(surftype) 99 | self.data['lenstype'].append(lenstype) 100 | self.data['lrad'].append(lrad) 101 | self.data['rCA'].append(rCA) 102 | self.data['rCA_short'].append(rCA_short) 103 | self.data['surf_decenter'].append(surf_decenter) 104 | self.data['surf_tilt'].append(surf_tilt) 105 | self.data['coating'].append(coating) 106 | if CT is not None: 107 | self.data['CT'].append(CT) 108 | self.data['material'].append(material) 109 | 110 | if surftype == 'flat': 111 | self.data['radius'].append(0) 112 | self.data['asph'].append([None, None]) 113 | self.data['radius2'].append(None) 114 | self.data['asph2'].append([None, None]) 115 | self.data['surf_rotation'].append(0) 116 | elif surftype in ['spherical', 'conical', 'polynominal', 'aspheric']: 117 | self.data['radius'].append(radius) 118 | self.data['asph'].append(asph) 119 | self.data['radius2'].append(None) 120 | self.data['asph2'].append([None, None]) 121 | self.data['surf_rotation'].append(0) 122 | elif surftype in ['cylindrical', 'conicylindrical', 'polycylindrical', 'acylindrical', 123 | 'toric', 'conitoric', 'polytoric', 'atoric']: 124 | """ 125 | Also for cylindric lenses, second variables have to be specified explicitly because cylinder might be along x- or y-axis. 126 | TODO: replace with surf_rotation parameter. 127 | """ 128 | self.data['radius'].append(radius) 129 | self.data['asph'].append(asph) 130 | self.data['radius2'].append(radius2) 131 | self.data['asph2'].append(asph2) 132 | self.data['surf_rotation'].append(surf_rotation) 133 | 134 | def reverse(self): 135 | # TODO: This needs to be overhauled to account for latest set of parameters 136 | for key in self.data: 137 | if key in ['radius', 'radius2']: 138 | self.data[key] = [-1.*r for r in self.data[key][::-1]] 139 | elif key in ['asph', 'asph2'] and self.data[key] != []: 140 | self.data[key] = [[e[0]] + [-1.*e[i] for i in range(1,len(e))] for e in self.data[key][::-1]] # conical constant remains unchanged 141 | elif key == 'surf_tilt' and self.data[key] != []: 142 | self.data[key] = [[-1.*e[0], -1.*e[1]] for e in self.data[key][::-1]] 143 | #elif key == 'glass': 144 | # self.data[key] = [self.data[key][:-1:-1]] + [self.data[key][-1]] 145 | else: 146 | self.data[key] = self.data[key][::-1] 147 | self.isreverse = not self.isreverse 148 | 149 | def decenter(self): 150 | pass -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_CDGM.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_CDGM.pkl -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_Hikari.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_Hikari.pkl -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_Hoya.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_Hoya.pkl -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_Ohara.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_Ohara.pkl -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_Schott.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_Schott.pkl -------------------------------------------------------------------------------- /raytrace/glasscatalog_data/glasscatalog_Sumita.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFHD/OptiCore/52b41a1815d3de2df6c02ec913cce162eeeca675/raytrace/glasscatalog_data/glasscatalog_Sumita.pkl -------------------------------------------------------------------------------- /raytrace/intersect_asphere.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from .intersect import rectangle_intersect 23 | from ..surface.radialprofiles import get_z_evenasphere, get_N_evenasphere 24 | from ..surface.toric import get_z_toric, get_N_toric 25 | 26 | """ 27 | PARKED CODE for spossible use of Newton-Raphson in the future 28 | 29 | def dzdt(k, A, O, D, t): 30 | pass 31 | 32 | def dzdr(c, k, A, r): 33 | pass 34 | 35 | def dz1dr(c, k, r): 36 | pass 37 | 38 | def dz2dr(A, r): 39 | r_dz2dr = sum([2*(i+2)*A[i]*r**(2*(i+2) - 1) for i in range(len(A))]) 40 | return r_dz2dr 41 | 42 | def dr2dt(O, D, t): 43 | # t is shape (N) 44 | # O,D are shape (N,3) 45 | 46 | D2 = D*D 47 | OD = O*D 48 | r_dr2dt = 2*(t*(D2[:,0] + D2[:,1]) + (OD[:,0] + OD[:,1])) 49 | return r_dr2dt 50 | 51 | def drdt(O, D, t, r): 52 | D2 = D*D 53 | OD = O*D 54 | O2 = O*O 55 | D2s = (D2[:,0] + D2[:,1]) 56 | ODs = (OD[:,0] + OD[:,1]) 57 | O2s = (O2[:,0] + O2[:,1]) 58 | r2 = t*t*D2s + 2*t*ODs + O2s 59 | r = np.sqrt(r2) 60 | r_drdt = (t*D2s + ODs) / r 61 | return r_drdt 62 | """ 63 | 64 | # configuration for the algorithms 65 | N1 = 17 66 | N2 = 16 # Not needed as long as rotational symmetry of lens is assumed 67 | N_BISECTION = 12 68 | ACCURACY = 1e-5 69 | MAX_ITERATIONS = 6 70 | 71 | def _get_bbox(lrad, zmin, zmax): 72 | # Quads for the 6 box sides 73 | V1 = [-lrad, -lrad, zmin] 74 | V2 = [lrad, -lrad, zmin] 75 | V3 = [lrad, lrad, zmin] 76 | V4 = [-lrad, lrad, zmin] 77 | V5 = [-lrad, -lrad, zmax] 78 | V6 = [lrad, -lrad, zmax] 79 | V7 = [lrad, lrad, zmax] 80 | V8 = [-lrad, lrad, zmax] 81 | # generate the sides Faces 82 | Qfront = [V1, V2, V3, V4] 83 | Qback = [V5, V6, V7, V8] 84 | Qleft = [V1, V4, V8, V5] 85 | Qright = [V2, V3, V7, V6] 86 | Qtop = [V4, V3, V7, V8] 87 | Qbottom = [V1, V2, V6, V5] 88 | 89 | return Qfront, Qback, Qleft, Qright, Qtop, Qbottom 90 | 91 | def intersect_asphere(O, D, C, lrad, rad, k, A): 92 | n_rays = O.shape[0] 93 | ones = np.ones(n_rays) 94 | 95 | # Step 1: transform ray into local coordinate system 96 | O = O - C 97 | 98 | ########################################################################### 99 | 100 | # generate bounding box 101 | r = np.linspace(-lrad, lrad, N1, endpoint = True) 102 | z = get_z_evenasphere(r**2, rad, k, A) 103 | zmin = 1.2*min(z) # 20 percent margin because of the coarse sampling of the surface 104 | zmax = 1.2*max(z) 105 | Qfront, Qback, Qleft, Qright, Qtop, Qbottom = _get_bbox(lrad, zmin, zmax) 106 | 107 | # intersect ray with bbox to find bracket for root-finding 108 | # initialize min and max t values, nan by defaul 109 | t0 = np.full((n_rays), float('nan')) 110 | t1 = np.full((n_rays), float('nan')) 111 | # try front-back first. should account for most rays 112 | t_front = rectangle_intersect(O, D, Qfront, return_t=True) 113 | t_back = rectangle_intersect(O, D, Qback, return_t=True) 114 | idx_front = np.isnan(t_front) 115 | idx_back = np.isnan(t_back) 116 | # ASSUMPTION: front can only be first, back only last == no rays travelling backwards 117 | # TODO: Deal with Ghosts, mirrors, and catadioptrics later after you confirmed this works 118 | t0[~idx_front] = t_front[~idx_front] 119 | t1[~idx_back] = t_back[~idx_back] 120 | # check sides for any where front and/or back failed 121 | if np.any(np.isnan(t0)) or np.any(np.isnan(t1)): 122 | t_left = rectangle_intersect(O, D, Qleft, return_t=True) 123 | t_right = rectangle_intersect(O, D, Qright, return_t=True) 124 | t_top = rectangle_intersect(O, D, Qtop, return_t=True) 125 | t_bottom = rectangle_intersect(O, D, Qbottom, return_t=True) 126 | idx_left = np.isnan(t_left) 127 | idx_right = np.isnan(t_right) 128 | idx_top = np.isnan(t_top) 129 | idx_bottom = np.isnan(t_bottom) 130 | # compare t to t_front and t_back to determin if hit must be a first or second 131 | t_all = np.column_stack((t_left, t_right, t_top, t_bottom)) 132 | t_min = np.nanmin(t_all, axis=1) 133 | t_max = np.nanmax(t_all, axis=1) 134 | t0[idx_front] = t_min[idx_front] 135 | t1[idx_back] = t_max[idx_back] 136 | 137 | # use iterative bisection method to find t 138 | n_iteration = 0 139 | while n_iteration < MAX_ITERATIONS: 140 | n_iteration = n_iteration + 1 141 | # generate the test points 142 | t_guess = np.linspace(t0, t1, N_BISECTION, endpoint=True) 143 | DT = np.einsum('ij,ki->ijk', D, t_guess) 144 | P_guess = O[:,:,np.newaxis] + DT 145 | # evaluate the z-distance from the surface 146 | r2_guess = np.einsum('ijk,ijk->ik', P_guess[:,:2,:], P_guess[:,:2,:]) 147 | z_guess = P_guess[:,2,:] 148 | z_surf = get_z_evenasphere(r2_guess, rad, k, A) 149 | dz_guess = z_guess - z_surf 150 | dz_guess_abs = np.abs(dz_guess) 151 | dz_min = np.min(dz_guess_abs, axis=1)#[:,np.newaxis] 152 | if not np.any(dz_min > ACCURACY): 153 | break 154 | # update guess for t 155 | # idx_min = np.argmin(dz_guess_abs, axis = 1) 156 | # t = np.choose(idx_min, t_guess) 157 | idx_gt = (dz_guess > 0).argmax(1) 158 | idx_gt[idx_gt == 0] = N_BISECTION - 1 159 | t0 = np.choose(idx_gt-1, t_guess)# - ACCURACY 160 | t1 = np.choose(idx_gt, t_guess)# + ACCURACY 161 | 162 | # final evaluation 163 | idx_min = np.argmin(dz_guess, axis = 1) 164 | t = np.choose(idx_min, t_guess) 165 | # discard rays that did not meet ACCURACY 166 | idx_accuracy = dz_min > ACCURACY 167 | t[idx_accuracy] = float('nan') 168 | 169 | # nfail = sum(idx_accuracy) 170 | # print(f'Aspheric trace finished after {n_iteration}/{MAX_ITERATIONS} iterations with {nfail}/{n_rays} failures') 171 | 172 | td = (t*D.T).T # The transpose here is because of the shapes of the arrays 173 | P = O + td # intersection point 174 | # C2 = [0, 0, rad] 175 | 176 | # calculate surface normal 177 | r = np.sqrt(np.einsum('ij,ij->i', P[:,:2], P[:,:2])) 178 | b = np.arctan2(P[:,1], P[:,0]) 179 | N = get_N_evenasphere(r, b, rad, k, A, returnformat='seqrt') 180 | N = (N.T/np.sqrt(np.einsum('ij,ij->i',N,N))).T 181 | # Flip N towards incoming ray direction 182 | s = np.sign(np.einsum('ij,ij->i', N, D)) 183 | s[s==0] = 1 184 | N = (N.T*s).T 185 | 186 | # TODO optional: use bisection method to get initial guess, then use Newton-Raphson for refinement 187 | 188 | ########################################################################### 189 | 190 | # Step 4: Transform point and normal back into original coordiante system 191 | P = P + C 192 | 193 | # Step 5: Check if Point lies within defined aperture 194 | diff1 = P[:,0]-C[0] 195 | diff2 = P[:,1]-C[1] 196 | Prad = np.sqrt(diff1*diff1 + diff2*diff2) 197 | P[Prad > lrad] = float('nan') # TODO: remove this, only create idx_fail, and let rays.update() take care of the rest ? 198 | 199 | 200 | idx_fail = np.isnan(P[:,0]) 201 | return P, N, t, idx_fail 202 | 203 | 204 | 205 | def intersect_implicit(O, D, C, lrad, z_fun, z_fun_params, N_fun=None): 206 | """ 207 | Intersection for a general implicit surface, 208 | i.e. one that takes the form f(x,y,z) = 0. 209 | This function needs to be supplied with a function reference (plus ccoefficients) 210 | z_fun so that z_fun(x, y, *z_fun_params) = z, 211 | as well as a function N_fun that calculates the local surface normal. 212 | The parameter list z_fun_params shall also apply to N_fun. 213 | TODO: Add option to numerically obtain N_fun from z_fun when there is no (known) analytic solution. 214 | """ 215 | n_rays = O.shape[0] 216 | ones = np.ones(n_rays) 217 | 218 | # Step 1: transform ray into local coordinate system 219 | O = O - C 220 | 221 | ########################################################################### 222 | 223 | # generate bounding box 224 | x_test = [0] 225 | y_test = [0] 226 | for i in range(N1): 227 | r = lrad*i/(N1 - 1) 228 | phi_list = np.linspace(0, 2*np.pi, N2, endpoint=False) 229 | x_new = [r*np.cos(phi) for phi in phi_list] 230 | y_new = [r*np.sin(phi) for phi in phi_list] 231 | x_test = x_test + x_new 232 | y_test = y_test + y_new 233 | x_test, y_test = np.array(x_test), np.array(y_test) 234 | z_test = z_fun(x_test, y_test, *z_fun_params) 235 | zmin = 1.2*min(z_test) # 20 percent margin because of the coarse sampling of the surface 236 | zmax = 1.2*max(z_test) 237 | Qfront, Qback, Qleft, Qright, Qtop, Qbottom = _get_bbox(lrad, zmin, zmax) 238 | 239 | # intersect ray with bbox to find bracket for root-finding 240 | # initialize min and max t values, nan by defaul 241 | t0 = np.full((n_rays), float('nan')) 242 | t1 = np.full((n_rays), float('nan')) 243 | # try front-back first. should account for most rays 244 | t_front = rectangle_intersect(O, D, Qfront, return_t=True) 245 | t_back = rectangle_intersect(O, D, Qback, return_t=True) 246 | idx_front = np.isnan(t_front) 247 | idx_back = np.isnan(t_back) 248 | # ASSUMPTION: front can only be first, back only last == no rays travelling backwards 249 | # TODO: Deal with Ghosts, mirrors, and catadioptrics later after you confirmed this works 250 | t0[~idx_front] = t_front[~idx_front] 251 | t1[~idx_back] = t_back[~idx_back] 252 | # check sides for any where front and/or back failed 253 | if np.any(np.isnan(t0)) or np.any(np.isnan(t1)): 254 | t_left = rectangle_intersect(O, D, Qleft, return_t=True) 255 | t_right = rectangle_intersect(O, D, Qright, return_t=True) 256 | t_top = rectangle_intersect(O, D, Qtop, return_t=True) 257 | t_bottom = rectangle_intersect(O, D, Qbottom, return_t=True) 258 | idx_left = np.isnan(t_left) 259 | idx_right = np.isnan(t_right) 260 | idx_top = np.isnan(t_top) 261 | idx_bottom = np.isnan(t_bottom) 262 | # compare t to t_front and t_back to determin if hit must be a first or second 263 | t_all = np.column_stack((t_left, t_right, t_top, t_bottom)) 264 | t_min = np.nanmin(t_all, axis=1) 265 | t_max = np.nanmax(t_all, axis=1) 266 | t0[idx_front] = t_min[idx_front] 267 | t1[idx_back] = t_max[idx_back] 268 | 269 | # use iterative bisection method to find t 270 | n_iteration = 0 271 | while n_iteration < MAX_ITERATIONS: 272 | n_iteration = n_iteration + 1 273 | # generate the test points 274 | t_guess = np.linspace(t0, t1, N_BISECTION, endpoint=True) 275 | DT = np.einsum('ij,ki->ijk', D, t_guess) 276 | P_guess = O[:,:,np.newaxis] + DT 277 | # evaluate the z-distance from the surface 278 | #r2_guess = np.einsum('ijk,ijk->ik', P_guess[:,:2,:], P_guess[:,:2,:]) 279 | x_guess = P_guess[:,0,:] 280 | y_guess = P_guess[:,1,:] 281 | z_guess = P_guess[:,2,:] 282 | z_surf = z_fun(x_guess, y_guess, *z_fun_params) 283 | #z_surf = get_z_evenasphere(r2_guess, rad, k, A) 284 | dz_guess = z_guess - z_surf 285 | dz_guess_abs = np.abs(dz_guess) 286 | dz_min = np.min(dz_guess_abs, axis=1)#[:,np.newaxis] 287 | if not np.any(dz_min > ACCURACY): 288 | break 289 | # update guess for t 290 | # idx_min = np.argmin(dz_guess_abs, axis = 1) 291 | # t = np.choose(idx_min, t_guess) 292 | idx_gt = (dz_guess > 0).argmax(1) 293 | idx_gt[idx_gt == 0] = N_BISECTION - 1 294 | t0 = np.choose(idx_gt-1, t_guess)# - ACCURACY 295 | t1 = np.choose(idx_gt, t_guess)# + ACCURACY 296 | 297 | # final evaluation 298 | idx_min = np.argmin(dz_guess, axis = 1) 299 | t = np.choose(idx_min, t_guess) 300 | # discard rays that did not meet ACCURACY 301 | idx_accuracy = dz_min > ACCURACY 302 | t[idx_accuracy] = float('nan') 303 | 304 | # nfail = sum(idx_accuracy) 305 | # print(f'Aspheric trace finished after {n_iteration}/{MAX_ITERATIONS} iterations with {nfail}/{n_rays} failures') 306 | 307 | td = (t*D.T).T # The transpose here is because of the shapes of the arrays 308 | P = O + td # intersection point 309 | 310 | # calculate surface normal 311 | x_final = P[:,0] 312 | y_final = P[:,1] 313 | N = N_fun(x_final, y_final, *z_fun_params, returnformat='seqrt') 314 | 315 | #r = np.sqrt(np.einsum('ij,ij->i', P[:,:2], P[:,:2])) 316 | #b = np.arctan2(P[:,1], P[:,0]) 317 | #N = get_N_evenasphere(r, b, rad, k, A, returnformat='seqrt') 318 | # N = (N.T/np.sqrt(np.einsum('ij,ij->i',N,N))).T 319 | # Flip N towards incoming ray direction 320 | s = np.sign(np.einsum('ij,ij->i', N, D)) 321 | s[s==0] = 1 322 | N = (N.T*s).T 323 | 324 | # optional: use bisection method to get initial guess, then use Newton-Raphson for refinement 325 | 326 | ########################################################################### 327 | 328 | # Step 4: Transform point and normal back into original coordiante system 329 | P = P + C 330 | 331 | # Step 5: Check if Point lies within defined aperture 332 | diff1 = P[:,0] # - C[0] 333 | diff2 = P[:,1] # - C[1] 334 | Prad = np.sqrt(diff1*diff1 + diff2*diff2) 335 | P[Prad > lrad] = float('nan') # TODO: remove this, only create idx_fail, and let rays.update() take care of the rest ? 336 | 337 | idx_fail = np.isnan(P[:,0]) 338 | return P, N, t, idx_fail -------------------------------------------------------------------------------- /raytrace/paraxial.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def refract(y, u, n0, n1, phi): 23 | u1 = (n0*u - y*phi) / n1 24 | return y, u1 25 | 26 | def transfer(y, u, t): 27 | y1 = y + u*t 28 | return y1, u 29 | 30 | def reflect(y, u, r): 31 | u1 = 2./r*y + u 32 | return y, u1 33 | 34 | def trace_lens(y, u, r_list, t_list, n_list, n_elements): 35 | """ 36 | n_elements is the number of lens elements, e.g. for a singlet n_elements=1. 37 | n_list contains the refractive indices including ambient medium befoire and after. length must be n_elements+2 (or longer, see comments in code). 38 | """ 39 | # trace a pair of surface and thickness 40 | for i in range(n_elements): 41 | # surface 42 | r = r_list[i] 43 | if r == 0: 44 | phi = 0 45 | else: 46 | phi = (n_list[i+1] - n_list[i]) / r 47 | y, u = refract(y, u, n_list[i], n_list[i+1], phi) 48 | # thickness 49 | y, u = transfer(y, u, t_list[i]) 50 | # trace the final surface 51 | r = r_list[i+1] 52 | if r == 0: 53 | phi = 0 54 | else: 55 | phi = (n_list[-1] - n_list[i+1]) / r 56 | y, u = refract(y, u, n_list[i+1], n_list[-1], phi) # n_list[-1] ensures the ambient medium is always taken, so that an overly long list can be taken and the execution covered by n_elements 57 | return y, u 58 | 59 | def calc_BFL(y, u): 60 | if u == 0: 61 | return float('inf') 62 | # t = -y/u 63 | t = -y/u 64 | return t 65 | 66 | def calc_EFL(y0, u1): 67 | if u1 == 0: 68 | return float('inf') 69 | # technically the same as calc_BFL, just separtate to make it clear the last u and first y are to be used 70 | t = -y0/u1 71 | return t -------------------------------------------------------------------------------- /raytrace/trace_sequential.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import time 21 | 22 | import numpy as np 23 | 24 | from . import intersect as rt_intersect 25 | from . import intersect_asphere as rt_intersect_asph 26 | from ..surface.toric import get_z_toric, get_N_toric 27 | 28 | import warnings 29 | warnings.filterwarnings('ignore') 30 | 31 | 32 | def exec_trace(lens, rays, surfs=None, trace_detector=True, t_detector=None, trace_reverse=False): 33 | """ 34 | This function performs a trace thorugh all surfaces 35 | 36 | Parameters 37 | ---------- 38 | lens: LensSystem() instance 39 | The Lens system to be traced 40 | rays: Rayfan() instance 41 | A Rayfan-class object containing the initial rays for tracing 42 | surfs : iterable 43 | List of surface indices in order which they shall be traced 44 | trace_detector: bool 45 | If True, performs the ray tracing to the sensor as the last step. 46 | If False, allows an external final trace, e.g. to the Blender scene 47 | t_detector: float 48 | If not None, the final rays will be projected by a fixed ray-t along their final direction 49 | trace_reverse: bool 50 | If True, handle first/last surface assumptions differently (detector assumed before first surface, not after last) 51 | 52 | Returns 53 | ------- 54 | rays: Rayfan() instance 55 | The updated Rayfan object containing the final ray positions 56 | 57 | """ 58 | # initialize variables 59 | if lens.surf_sequence is not None: 60 | surfs = lens.surf_sequence 61 | elif surfs is None: 62 | # default to just tracing straight through the surfaces 63 | surfs = [i+1 for i in range(lens.num_surfaces)] 64 | if trace_reverse: 65 | lastsurface = surfs[0] + 1 66 | else: 67 | lastsurface = surfs[0] - 1 68 | n_surfs = len(surfs) 69 | ld = lens.data # abbreviation 70 | reflectionstate = 1 # TODO: merge this into a better direction handling mechanism 71 | 72 | # start ray tracing loop over surfaces 73 | for i, idx_s in enumerate(surfs): 74 | # current direction of propagation 75 | direction = int(np.sign(idx_s - lastsurface)) 76 | # check if the next (not current!) surface is identical to the last surface. 77 | # In this case, a reflection must happen, else a refraction. 78 | refract = 1 79 | ismirror = ld['ismirror'][idx_s] 80 | ismirror_pre = ld['ismirror'][idx_s-1] 81 | mirrorfactor = 1 - 2*ismirror_pre 82 | reflectionstate = reflectionstate*mirrorfactor 83 | if i < (n_surfs-1): 84 | nextsurface = surfs[i+1] 85 | elif trace_reverse: 86 | nextsurface = 0 87 | else: 88 | nextsurface = max(surfs)+1 89 | if nextsurface == lastsurface: 90 | # case ghost 91 | refract = -1 92 | if ismirror: 93 | # case mirror 94 | refract = -1 95 | 96 | # get surface parameters 97 | rad = ld['rCA'][idx_s] 98 | r = ld['radius'][idx_s] 99 | k = ld['asph'][idx_s][0] 100 | A = ld['asph'][idx_s][1:] 101 | r2 = ld['radius2'][idx_s] 102 | k2 = ld['asph2'][idx_s][0] 103 | A2 = ld['asph2'][idx_s][1:] 104 | # determine the type of surface w.r.t. the intersection algorithms: 105 | surftype = ld['type'][idx_s] 106 | # surface rotation for e.g. cylinder lenses 107 | surf_rotation = ld['surf_rotation'][idx_s] 108 | 109 | # set center of sphere coordinates 110 | # TODO: Add decenter and tilt 111 | C = [0, 0, ld['CT_sum'][idx_s] + r] 112 | C = np.array(C) 113 | C_CT = [0, 0, ld['CT_sum'][idx_s]] 114 | C_CT = np.array(C_CT) 115 | 116 | # flip inside/outside IOR depending on direction 117 | if direction == 1: 118 | n1, n2 = ld['n'][lastsurface], ld['n'][idx_s] 119 | else: 120 | n1, n2 = ld['n'][lastsurface-1], ld['n'][idx_s-1] 121 | 122 | # get the current rays 123 | O, D = rays.get_rays() 124 | 125 | # Check if aperture is involved 126 | # A more universal way would be to always trace each aperture and check for positive ray-t, 127 | # comparing with the ray-t of the following lens-surface; 128 | # That would allow more universal placement but might slow down a lot. 129 | # ['idx_surface', 'z_ap', 'shape', 'radius', 'n_blades'] 130 | for j, ap in lens.apertures.items(): 131 | aperture_here = False 132 | if ap['idx_surface'] is not None: 133 | if ap['idx_surface'] == idx_s-1 and direction == 1: 134 | aperture_here = True 135 | elif ap['idx_surface'] == idx_s and direction == -1: 136 | aperture_here = True 137 | else: 138 | continue 139 | else: 140 | # TODO: Check if aperture is between previous and next surface to save unneccessary tracing 141 | aperture_here=True 142 | if ap['z_ap']: 143 | zap = ap['z_ap'] 144 | else: 145 | zap = ld['CT_sum'][idx_s] 146 | if aperture_here: 147 | r_ap = ap['radius'] 148 | n_blades = ap['n_blades'] 149 | P_inside, P_outside, N, idx_fail_i, idx_fail_o = rt_intersect.aperture_intersect(O, D, r_ap, zap, n_blades=n_blades) 150 | rays.update_special_hits(P_outside, idx_fail_i) 151 | O[~idx_fail_o] = float('nan') 152 | 153 | # calculate lens intersection points 154 | if surftype == 'aspheric': 155 | P, N, t, idx_fail = rt_intersect_asph.intersect_asphere(O, D, C_CT, rad, r, k, A) 156 | elif surftype == 'toric': 157 | z_fun_params = [r, r2, surf_rotation] 158 | P, N, t, idx_fail = rt_intersect_asph.intersect_implicit(O, D, C_CT, rad, get_z_toric, z_fun_params, 159 | N_fun=get_N_toric) 160 | else: 161 | P, N, t, idx_fail = rt_intersect.lens_intersect(O, D, C_CT, r, rad, 162 | k=k, A=A, surf_rotation=surf_rotation, surfshape=surftype, direction=direction)#*reflectionstate) 163 | 164 | # calculate new ray directions 165 | if refract == 1: 166 | D_new = rt_intersect.refract_ray(D, N, n1, n2) 167 | else: 168 | D_new = rt_intersect.reflect_ray(D, N) 169 | 170 | # Adjust ray intensity by bulk transmission. 171 | # Need value from next or previous surface depending on direction. 172 | I_new = None 173 | if direction == 1: 174 | t10 = ld['t10'][lastsurface] 175 | else: 176 | t10 = ld['t10'][idx_s] 177 | if t10 is not None: 178 | if t10 == 1: 179 | # Skip computation for 100% transmission 180 | pass 181 | elif t10 == 0: 182 | # No transmission effectively means fail 183 | P[:,:] = float('nan') 184 | idx_fail = np.isnan(P[:,0]) 185 | rays.update(P, D_new, None, None, idx_fail, N=N) 186 | return rays 187 | else: 188 | I = rays.get_I() 189 | bulk_factor = t10**(t/10) 190 | I_new = I*bulk_factor 191 | 192 | # Adjust ray intensity by coating: 193 | coating = ld['coating'][idx_s] 194 | if coating[0] is not None: 195 | if I_new is None: 196 | I = rays.get_I() 197 | else: 198 | I = I_new 199 | ckey = coating[0] 200 | if ckey == 'MIRROR': 201 | I_new = I 202 | elif ckey == 'FRESNEL_0': 203 | n_ratio = n2/n1 204 | R_coating = lens.coating_data[ckey].get_R(n_ratio) 205 | if refract == 1: 206 | I_new = I*(1.-R_coating) 207 | else: 208 | I_new = I*R_coating 209 | elif ckey.startswith('DATA_'): 210 | R_coating = lens.coating_data[ckey].get_R(lens.wl, coating[1][2]) 211 | if refract == 1: 212 | I_new = I*(1.-R_coating) 213 | else: 214 | I_new = I*R_coating 215 | elif ckey.startswith('FIXVALUE_'): 216 | R_coating = lens.coating_data[ckey].get_R() 217 | if refract == 1: 218 | I_new = I*(1.-R_coating) 219 | else: 220 | I_new = I*R_coating 221 | else: 222 | print(f"[OC]: Warning: invalid coating key {ckey} at surface {idx_s}") 223 | I_new = I 224 | 225 | # update the origin and direction arrays 226 | rays.update(P, D_new, I_new, None, idx_fail, N=N) 227 | 228 | # update parameters for next loop 229 | lastsurface = int(1*idx_s) 230 | 231 | if np.all(idx_fail): 232 | return rays 233 | 234 | # additional aperture check before the detector 235 | O, D = rays.get_rays() 236 | for j, ap in lens.apertures.items(): 237 | aperture_here = False 238 | if ap['idx_surface'] is not None: 239 | if idx_s == ap['idx_surface']: # idx_s has retained the value of the last surface from the loop 240 | aperture_here = True 241 | else: 242 | continue 243 | else: 244 | # TODO: Check if aperture is between previous and next surface to save unneccessary tracing 245 | aperture_here=True 246 | if ap['z_ap']: 247 | zap = ap['z_ap'] 248 | else: 249 | zap = ld['CT_sum'][idx_s] 250 | if aperture_here: 251 | r_ap = ap['radius'] 252 | n_blades = ap['n_blades'] 253 | P_inside, P_outside, N, idx_fail_i, idx_fail_o = rt_intersect.aperture_intersect(O, D, r_ap, zap, n_blades=n_blades, pass_inside=True) 254 | rays.update_special_hits(P_outside, idx_fail_o) 255 | # reset the origin rays 256 | rays.update(O, D, None, None, idx_fail_i, N=N) # use idx_fail_i because aperture_intersect has opposite logic (aperture IS hit) 257 | 258 | # trace detector 259 | if trace_detector: 260 | O, D = rays.get_rays() 261 | if t_detector is None: 262 | P, N, t, idx_fail = rt_intersect.rectangle_intersect(O, D, lens.detector['Quad']) 263 | else: 264 | P = O + (t_detector*D.T).T 265 | N = None 266 | idx_fail = np.isnan(P[:,0]) 267 | rays.D_tosensor = np.array(rays.D) 268 | rays.update(P, None, None, None, idx_fail, N=N) 269 | 270 | return rays -------------------------------------------------------------------------------- /surface/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | from .flat import * 21 | from .parabolic import * 22 | from .spherical import * 23 | from .aspheric import * 24 | from .rim import * 25 | from .toric import * 26 | from .sagsurface import * 27 | 28 | 29 | def add_surface(ltype, surf_subtype, squarelens, N1, N2, lrad, 30 | srad, k=None, A=None, RY2=None, k2=None, A2=None, 31 | surf_rotation=0, 32 | zadd=0, nVerts=0, 33 | dshape=False, lrad_ext=None): 34 | """ 35 | This function is a wrapper for the different surface shapes 36 | and returns the corresponding vertices, surface index lists and normals 37 | 38 | to keep the code short observe the following variable names: 39 | v, f, n == verts, faces, normals 40 | """ 41 | 42 | """Rectangular cutout""" 43 | if squarelens: 44 | if ltype == 'flat' or surf_subtype == 'flat': 45 | # flat surface 46 | v, f, n, v_outline = add_sqflat_surface(lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape) 47 | elif ltype == 'rotational' and surf_subtype == 'spherical': 48 | # normal spherical surface 49 | v, f, n, v_outline = add_sqspherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lwidth_ext=lrad_ext) 50 | elif ltype == 'rotational' and surf_subtype != 'spherical': 51 | # normal aspheric surface 52 | v, f, n, v_outline = add_sagsurface_rectangular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='aspheric', dshape=dshape, lrad_ext=lrad_ext) 53 | elif ltype == 'cylindrical' and surf_subtype == 'spherical': 54 | # normal aspheric surface 55 | v, f, n, v_outline = add_sagsurface_rectangular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='cylindrical', dshape=dshape, lrad_ext=lrad_ext) 56 | elif ltype == 'cylindrical' and surf_subtype != 'spherical': 57 | # normal aspheric surface 58 | v, f, n, v_outline = add_sagsurface_rectangular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='acylindrical', dshape=dshape, lrad_ext=lrad_ext) 59 | """ 60 | elif ltype == 'cylindricX' and surf_subtype == 'spherical': 61 | # X-cylinder spherical 62 | v, f, n = sfc.add_sqspherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lwidth_ext=lrad_ext, cylinderaxis='X') 63 | elif ltype == 'cylindricY' and surf_subtype == 'spherical': 64 | # Y-cylinder spherical 65 | v, f, n = sfc.add_sqspherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lwidth_ext=lrad_ext, cylinderaxis='Y') 66 | """ 67 | elif ltype == 'toric' and surf_subtype == 'spherical': 68 | v, f, n, v_outline = add_sagsurface_rectangular(srad, k, A, lrad, N1, N2, zadd=zadd, nVerts=nVerts, 69 | rad2=RY2, k2=None, A2=None, 70 | surf_rotation=surf_rotation, surftype='toric',dshape=dshape, lrad_ext=lrad_ext) 71 | else: 72 | # in case of anything not covered by the above 73 | print("This surface combination is not implemented:", ltype, surf_subtype, squarelens) 74 | return None, None, None, None, None 75 | else: 76 | if ltype == 'flat' or surf_subtype == 'flat': #flat surface case 77 | v, f, n = add_flat_surface(lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape) 78 | v_outline = v # trivial 79 | elif ltype == 'rotational' and surf_subtype == 'spherical': 80 | v, f, n = add_spherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lrad_ext=lrad_ext) 81 | v_outline = v[-N2:] 82 | elif ltype == 'rotational' and surf_subtype != 'spherical': 83 | v, f, n = add_sagsurface_circular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='aspheric', dshape=dshape, lrad_ext=lrad_ext) 84 | v_outline = v[-N2:] 85 | elif ltype == 'cylindrical' and surf_subtype == 'spherical': 86 | v, f, n = add_sagsurface_circular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='cylindrical', dshape=dshape, lrad_ext=lrad_ext) 87 | v_outline = v[-N2:] 88 | elif ltype == 'cylindrical' and surf_subtype != 'spherical': 89 | v, f, n = add_sagsurface_circular(srad, k, A, lrad, N1, N2, surf_rotation=surf_rotation, zadd=zadd, nVerts=nVerts, surftype='acylindrical', dshape=dshape, lrad_ext=lrad_ext) 90 | v_outline = v[-N2:] 91 | """ 92 | elif ltype == 'cylindricX' and surf_subtype == 'spherical': 93 | v, f, n = sfc.add_spherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lrad_ext=lrad_ext, cylinderaxis='X') 94 | elif ltype == 'cylindricY' and surf_subtype == 'spherical': 95 | v, f, n = sfc.add_spherical_surface(srad, lrad, N1, N2, zadd=zadd, nVerts=nVerts, dshape=dshape, lrad_ext=lrad_ext, cylinderaxis='Y') 96 | """ 97 | elif ltype == 'toric' and surf_subtype == 'spherical': 98 | v, f, n = add_sagsurface_circular(srad, k, A, lrad, N1, N2, zadd=zadd, nVerts=nVerts, 99 | rad2=RY2, k2=None, A2=None, 100 | surf_rotation=surf_rotation, surftype='toric',dshape=dshape, lrad_ext=lrad_ext) 101 | v_outline = v[-N2:] 102 | else: 103 | print("This surface combination is not implemented:", ltype, surf_subtype, squarelens) 104 | return None, None, None, None, None 105 | 106 | N_inside_sq = 'this_will_fail' # variable should later be overwritten, this way i will catch errors more easily 107 | if squarelens: 108 | # this variable is used later for side face generation 109 | if ltype == 'flat' or surf_subtype == 'flat': 110 | N_inside_sq = 2 111 | else: 112 | N_inside_sq = N2 113 | 114 | # reorder the coordinate system to Blender covnention 115 | v = [[t[2], t[0], t[1]] for t in v] 116 | n = [[t[2], t[0], t[1]] for t in n] 117 | v_outline = [[t[2], t[0], t[1]] for t in v_outline] 118 | 119 | return v, f, n, N_inside_sq, v_outline -------------------------------------------------------------------------------- /surface/aspheric.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from .radialprofiles import get_z_evenasphere, get_N_evenasphere, get_dzdr_evenasphere 23 | 24 | def _check_k(k, r, lrad): 25 | #check if k is too large, else crop 26 | comp = (r/lrad)**2 - 1 27 | if k > comp*0.9999: 28 | k = comp*0.9999 29 | return k 30 | 31 | 32 | 33 | def add_aspheric_surface(R, k, A, lrad, N1, N2, zadd=0, nVerts=0, dshape=False, lrad_ext=0): 34 | """ 35 | zadd has to be set for second surface (only) 36 | """ 37 | 38 | """ 39 | return add_sagsurface_circular(rad, k, A, lrad, N1, N2, 40 | surftype="aspheric", 41 | zadd=zadd, nVerts=nVerts, dshape=dshape, lrad_ext=lrad_ext) 42 | """ 43 | 44 | verts = [] 45 | faces = [] 46 | normals = [] 47 | 48 | maxb = 2*np.pi 49 | if dshape: 50 | maxb = np.pi*N2/(N2-1) 51 | 52 | k = _check_k(k, R, lrad) 53 | 54 | verts.append([-zadd,0,0]) 55 | normals.append((1,0,0)) 56 | r = lrad/(N1- (lrad_ext > lrad)) 57 | x = get_z_evenasphere(r**2, R, k, A) 58 | for j in range(N2): 59 | b = maxb*j/N2 60 | verts.append([-1.*x-zadd, r*np.sin(b), r*np.cos(b)]) 61 | N = get_N_evenasphere(r, b, R, k, A) 62 | normals.append(N) 63 | if dshape and j==N2-1: 64 | pass 65 | else: 66 | fi1 = nVerts 67 | fi2 = fi1 + ((j+1)%N2 + 1) 68 | fi3 = fi1 + (j + 1) 69 | faces.append([fi1, fi2, fi3]) 70 | for i in range(1,N1 - (lrad_ext > lrad)): 71 | r = lrad*(i+1)/(N1 - (lrad_ext > lrad)) 72 | x = get_z_evenasphere(r**2, R, k, A) 73 | for j in range(N2): 74 | b = maxb*j/N2 75 | verts.append([-1.*x-zadd,r*np.sin(b),r*np.cos(b)]) 76 | N = get_N_evenasphere(r, b, R, k, A) 77 | normals.append(N) 78 | if dshape and j==N2-1: 79 | pass 80 | else: 81 | fi1 = nVerts + ((j+1) + i*N2) 82 | fi2 = nVerts + ((j+1)%N2 + 1 + i*N2) 83 | fi3 = fi2 - N2 84 | fi4 = fi1 - N2 85 | faces.append([fi4,fi3,fi2,fi1]) 86 | 87 | #if there is flat annulus 88 | if lrad_ext > lrad: 89 | i = N1 - 2 90 | r = lrad_ext 91 | for j in range(N2): 92 | b = maxb*j/N2 93 | verts.append([-1.*x - zadd,r*np.sin(b),r*np.cos(b)]) 94 | normals.append((1,0,0)) 95 | if dshape and j==N2-1: 96 | pass 97 | else: 98 | fi1 = nVerts + (j+i*N2+1)+N2 99 | fi2 = nVerts + ((j+1)%N2+i*N2+1) + N2 100 | fi3 = fi2 - N2 101 | fi4 = fi1 - N2 102 | faces.append([fi4,fi3,fi2,fi1]) 103 | 104 | return verts, faces, normals 105 | 106 | def add_sqaspheric_surface(R, k, A, lwidth, N1, N2, nsurf=1, zadd=0, nVerts=0, cylindrical=False): 107 | """ 108 | nsurf=1 for first surface, 109 | nsurf=-1 for second surface 110 | 111 | zadd has to be set for second surface (only) 112 | """ 113 | 114 | verts = [] 115 | faces = [] 116 | vertquads = [] 117 | normals = [] 118 | 119 | if cylindrical: 120 | testrad = lwidth/2 121 | else: 122 | testrad = lwidth/2*np.sqrt(2) 123 | k = _check_k(k, R, testrad) 124 | 125 | for i in range(N1): 126 | y = lwidth*(i/(N1-1) - 0.5) 127 | for j in range(N2): 128 | z = lwidth*(j/(N2-1) - 0.5) 129 | if cylindrical: 130 | r = y 131 | else: 132 | r = np.sqrt(y**2 + z**2) 133 | x = get_z_evenasphere(r**2, R, k, A) 134 | verts.append([-1.*x*nsurf-zadd,y,z]) 135 | dxdr = get_dzdr_evenasphere(r, R, k, A) 136 | adxdr = np.sqrt(dxdr**2 + 1) 137 | dxdr = dxdr/adxdr 138 | if cylindrical: 139 | b = np.pi/2 140 | else: 141 | b = np.arctan2(y,z) 142 | normals.append((nsurf/adxdr, dxdr*np.sin(b), dxdr*np.cos(b))) 143 | ang = np.arctan2(z+lwidth/(2*N2-2),y+lwidth/(2*N1-2)) 144 | cond1 = N1%2 == 0 145 | cond2 = j==int(N2/2 - 1) or i==int(N1/2 - 1) 146 | if cond1 and cond2: 147 | vertquads.append(-99) 148 | else: 149 | vertquads.append(ang/np.pi*2) 150 | 151 | for i in range(N1-1): 152 | for j in range(N2-1): 153 | f1 = nVerts + j + 1 + N2*i 154 | f2 = nVerts + j + N2*i 155 | f3 = nVerts + j + N2*(i+1) 156 | f4 = nVerts + j + 1 + N2*(i+1) 157 | if vertquads[j+i*N2] >= 1: 158 | faces.append([f1,f2,f4][::nsurf]) 159 | faces.append([f2,f3,f4][::nsurf]) 160 | elif vertquads[j+i*N2] >= 0: 161 | faces.append([f1,f2,f3][::nsurf]) 162 | faces.append([f1,f3,f4][::nsurf]) 163 | elif vertquads[j+i*N2] >= -1: 164 | faces.append([f1,f2,f4][::nsurf]) 165 | faces.append([f2,f3,f4][::nsurf]) 166 | elif vertquads[j+i*N2] >= -2: 167 | faces.append([f1,f2,f3][::nsurf]) 168 | faces.append([f1,f3,f4][::nsurf]) 169 | else: 170 | faces.append([f1,f2,f3,f4][::nsurf]) 171 | 172 | return verts, faces, normals -------------------------------------------------------------------------------- /surface/cylindrical.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from .radialprofiles import get_zN_spherical, get_z_evenasphere, get_N_evenasphere 23 | from .surfaceutils import get_xy_rotatedsurface 24 | from ..utils.geometry import rotate_vector_x, rotate_vector_z 25 | 26 | def get_zN_cylindrical(x, y, R, surf_rotation=0, coord_input='original', returnformat=''): 27 | if coord_input == 'original' and not surf_rotation == 0: 28 | # rotate the coordinates to align with the cylidner axis 29 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 30 | r = np.abs(x) 31 | phi = np.pi*(x<0) 32 | z, N = get_zN_spherical(r, phi, R, returnformat='') 33 | if returnformat=='seqrt': 34 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 35 | else: 36 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 37 | # N = rotate_vector_x(N, surf_rotation=surf_rotation) 38 | return z, N 39 | 40 | def get_z_cylindrical(): 41 | pass 42 | 43 | def get_N_cylindrical(): 44 | pass 45 | 46 | def get_zN_acylindrical(x, y, R, k=0, A=[0], surf_rotation=0, coord_input='original', returnformat=''): 47 | if coord_input == 'original' and not surf_rotation == 0: 48 | # rotate the coordinates to align with the cylidner axis 49 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 50 | # the nominal orientation of the cylinder (surf_rotation == 0) is along the x-axis 51 | # hence the radial distance for the cylinder case is along the x-axis 52 | r = np.abs(x) 53 | r2 = x**2 54 | phi = np.pi*(x<0) 55 | z = get_z_evenasphere(r2, R, k, A) 56 | N = get_N_evenasphere(r, phi, R, k, A, returnformat=returnformat) 57 | if returnformat=='seqrt': 58 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 59 | else: 60 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 61 | # N = rotate_vector_x(N, surf_rotation=surf_rotation) 62 | return z, N 63 | 64 | def get_z_acylindrical(x, y, R, k=0, A=[0], surf_rotation=0, coord_input='original'): 65 | if coord_input == 'original' and not surf_rotation == 0: 66 | # rotate the coordinates to align with the cylidner axis 67 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 68 | # the nominal orientation of the cylinder (surf_rotation == 0) is along the x-axis 69 | # hence the radial distance for the cylinder case is along the x-axis 70 | r2 = x**2 71 | return get_z_evenasphere(r2, R, k, A) 72 | 73 | def get_N_acylindrical(x, y, R, k=0, A=[0], surf_rotation=0, coord_input='original', returnformat=''): 74 | if coord_input == 'original' and not surf_rotation == 0: 75 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 76 | r = np.abs(x) 77 | phi = np.pi*(x<0) 78 | N = get_N_evenasphere(r, phi, R, k, A, returnformat=returnformat) 79 | if returnformat=='seqrt': 80 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 81 | else: 82 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 83 | # N = rotate_vector_x(N, surf_rotation=surf_rotation) 84 | return N -------------------------------------------------------------------------------- /surface/flat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def add_flat_surface(lrad, N1, N2, zadd=0, xadd=0, nVerts=0, hole=False, hrad=0, dshape=False): 23 | """Flat surface with circular cross-section""" 24 | 25 | verts = [] 26 | faces = [] 27 | 28 | minb = 0 29 | maxb = 2*np.pi 30 | if dshape: 31 | minb = -np.pi/2 32 | maxb = np.pi*N2/(N2-1) 33 | 34 | 35 | if hole: 36 | for j in range(N2): 37 | b = maxb*j/N2 + minb 38 | verts.append([hrad*np.cos(b) + xadd, hrad*np.sin(b), -zadd]) 39 | for j in range(N2): 40 | b = maxb*j/N2 + minb 41 | verts.append([lrad*np.cos(b) + xadd, lrad*np.sin(b), -zadd]) 42 | 43 | if hole: 44 | for j in range(N2): 45 | fi1 = nVerts + (j+1)%N2 46 | fi2 = nVerts + j 47 | fi4 = fi1 + N2 48 | fi3 = fi2 + N2 49 | faces.append([fi1,fi2,fi3,fi4]) 50 | else: 51 | faces.append([int(nVerts + x) for x in range(N2)]) 52 | 53 | #define normals 54 | normals = (N2*(1+hole))*[[0, 0, 1]] 55 | 56 | return verts, faces, normals 57 | 58 | 59 | def add_sqflat_surface(lwidth, N1, N2, zadd=0, xadd=0, nVerts=0, dshape=False): 60 | """Flat surface with square cross-section""" 61 | 62 | verts = [] 63 | vo = [] 64 | N_tot = 2*(N1+N2) - 4 #number of points around the outline 65 | 66 | if dshape: 67 | x0 = 0 68 | else: 69 | x0 = -lwidth 70 | 71 | """Vertices added in same order as for sqspherical, but outline only""" 72 | # left side 73 | for i in range(N2): 74 | y = 2*(i/(N2-1) - 0.5)*lwidth 75 | verts.append([x0, y, -zadd]) 76 | # bottom and top, alternating 77 | for i in range(1, N1-1): 78 | if dshape: 79 | x = (i/(N1-1))*lwidth 80 | else: 81 | x = 2*(i/(N1-1) - 0.5)*lwidth 82 | verts.append([x, -lwidth, -zadd]) 83 | verts.append([x, lwidth, -zadd]) 84 | # right side 85 | for i in range(N2): 86 | y = 2*(i/(N2-1) - 0.5)*lwidth 87 | verts.append([lwidth, y, -zadd]) 88 | 89 | # Face construction: left-b2t + top-l2r + right-t2b + bottom-r2l 90 | faces = [i for i in range(N2)] + [2*i + 1 + N2 for i in range(N1-2)] + [i + N2 + 2*(N1 - 2) for i in range(N2)[::-1]] + [2*i + N2 for i in range(N1-2)[::-1]] 91 | # Face construction: bottom-l2r + right-b2t + top-r2l + left-t2b 92 | faces = [2*i + N2 for i in range(N1-2)] + [i + N2 + 2*(N1 - 2) for i in range(N2)] + [2*i + 1 + N2 for i in range(N1-2)[::-1]] + [i for i in range(N2)][::-1] 93 | 94 | faces = [[x + nVerts for x in faces]] 95 | 96 | #define normals 97 | normals = N_tot*[[0, 0, 1]] 98 | 99 | # reorder outline verts to go around 100 | if not dshape: 101 | vo_row1 = verts[:N1] 102 | vo_row2 = verts[-N1:] 103 | vo_col1 = verts[N1:-N1][::2] 104 | vo_col2 = verts[N1:-N1][1::2] 105 | vo = vo_row1[::-1] + vo_col1 + vo_row2+ vo_col2[::-1] 106 | 107 | return verts, faces, normals, vo -------------------------------------------------------------------------------- /surface/parabolic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def getdzdr(r,A): 23 | return 2.*r*A 24 | 25 | def add_parabolic_surface(fp, mrad, N1, N2, theta, orig='FP', zadd=0, nVerts=0, hole=False, hrad=0): 26 | """ 27 | nsurf=1 for first surface, 28 | nsurf=-1 for second surface 29 | 30 | zadd has to be set for second surface (only) 31 | 32 | orig: FP=focal point, MC = mirror center 33 | """ 34 | 35 | verts = [] 36 | faces = [] 37 | normals = [] 38 | 39 | fp *= -1 40 | nhole = not hole 41 | 42 | #compute basic paramters 43 | ct = np.cos(theta*2*np.pi/360) 44 | st = np.sin(theta*2*np.pi/360) 45 | fs = 2*fp/(1 + ct) 46 | A = 1/(4*fp) 47 | OAD = fs*st 48 | zOAD = OAD**2*A 49 | zmin = 0 50 | 51 | if orig=='FP': 52 | zoffset = -fp 53 | xoffset = 0 54 | fzoffset = -fp + zmin 55 | fxoffset = OAD 56 | elif orig == 'MC': 57 | zoffset = -zOAD 58 | xoffset = -OAD 59 | fzoffset = -zOAD + zmin 60 | fxoffset = 0 61 | 62 | if not hole: 63 | verts.append([OAD + xoffset, 0, A*OAD**2 + zoffset - zadd]) 64 | dzdr = getdzdr(OAD, A) 65 | adzdr = np.sqrt(1 + dzdr**2) 66 | dzdr = dzdr/adzdr 67 | b = np.pi/2 68 | normals.append((-1*dzdr*np.sin(b), -1*dzdr*np.cos(b), 1/adzdr)) 69 | 70 | for j in range(N2): 71 | ri = mrad/N1 72 | if hole: 73 | ri = hrad 74 | tj = 2*np.pi*j/N2 75 | ctj = np.cos(tj) 76 | stj = np.sin(tj) 77 | xp = OAD + ri*ctj 78 | yp = ri*stj 79 | dp = np.sqrt(xp**2 + yp**2) 80 | zp = A*dp**2 81 | verts.append([xp + xoffset, yp, zp + zoffset - zadd]) 82 | dzdr = getdzdr(dp,A) 83 | adzdr = np.sqrt(1 + dzdr**2) 84 | dzdr = dzdr/adzdr 85 | sinb = xp/dp 86 | cosb = yp/dp 87 | normals.append((-1*dzdr*sinb, -1*dzdr*cosb, 1/adzdr)) 88 | if not hole: 89 | fi1 = nVerts 90 | fi2 = fi1 + (j+1)%N2 + 1 91 | fi3 = fi1 + j + 1 92 | faces.append([fi3,fi2,fi1]) 93 | 94 | for i in range(1, N1): 95 | ri = mrad*(i+1)/N1 96 | if hole: 97 | ri = hrad + (mrad - hrad)*i/(N1 - 1) 98 | for j in range(N2): 99 | tj = 2*np.pi*j/N2 100 | ctj = np.cos(tj) 101 | stj = np.sin(tj) 102 | xp = OAD + ri*ctj 103 | yp = ri*stj 104 | dp = np.sqrt(xp**2 + yp**2) 105 | zp = A*dp**2 106 | verts.append([xp + xoffset, yp, zp + zoffset - zadd]) 107 | dzdr = getdzdr(dp, A) 108 | adzdr = np.sqrt(1 + dzdr**2) 109 | dzdr = dzdr/adzdr 110 | sinb = xp/dp 111 | cosb = yp/dp 112 | normals.append((-1*dzdr*sinb, -1*dzdr*cosb, 1/adzdr)) 113 | fi1 = nVerts + nhole + i*N2 + j 114 | fi2 = nVerts + nhole + i*N2 + (j+1)%N2 115 | fi3 = fi2 - N2 116 | fi4 = fi1 - N2 117 | faces.append([fi1,fi2,fi3,fi4]) 118 | 119 | zs = [v[2] for v in verts] 120 | fzoffset = min(zs) 121 | 122 | return verts, faces, fxoffset, fzoffset, normals -------------------------------------------------------------------------------- /surface/radialprofiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | """ flat """ 23 | 24 | def get_zN_flat(r, phi, returnformat=''): 25 | z = r*np.cos(phi) 26 | if returnformat=='seqrt': 27 | num = r.shape[0] 28 | zeros = np.zeros(num) 29 | ones = np.ones(num) 30 | N = np.column_stack((zeros, zeros, ones)) 31 | else: 32 | N = (0, 0, 1) 33 | #N = (1, 0, 0) 34 | return z, N 35 | 36 | def get_z_flat(r, phi): 37 | z = r*np.cos(phi) 38 | return z 39 | 40 | def get_N_flat(r, phi, returnformat=''): 41 | if returnformat=='seqrt': 42 | num = r.shape[0] 43 | zeros = np.zeros(num) 44 | ones = np.ones(num) 45 | N = np.column_stack((zeros, zeros, ones)) 46 | else: 47 | N = (0, 0, 1) 48 | #N = (1, 0, 0) 49 | return N 50 | 51 | """ spherical """ 52 | 53 | def get_zN_spherical(r, phi, R, returnformat=''): 54 | sign_R = 1 - 2*(R < 0) 55 | R = np.abs(R) 56 | sqt = np.sqrt(R**2 - r**2) 57 | z = (R - sqt)*sign_R 58 | dzdr = r/sqt*sign_R 59 | adzdr = np.sqrt(dzdr**2 + 1) 60 | dzdr = dzdr/adzdr 61 | if returnformat=='seqrt': 62 | N = np.column_stack((-dzdr*np.sin(phi), -dzdr*np.cos(phi), 1/adzdr)) 63 | else: 64 | N = (dzdr*np.cos(phi), dzdr*np.sin(phi), 1./adzdr) 65 | # N = (1./adzdr, dzdr*np.cos(phi), dzdr*np.sin(phi)) 66 | return z, N 67 | 68 | def get_z_spherical(r, R): 69 | sign_R = 1 - 2*(R < 0) 70 | R = np.abs(R) 71 | sqt = np.sqrt(R**2 - r**2) 72 | z = (R - sqt)*sign_R 73 | return z 74 | 75 | def get_N_spherical(r, phi, R, returnformat=''): 76 | sign_R = 1 - 2*(R < 0) 77 | sqt = np.sqrt(R**2 - r**2) 78 | dzdr = r/sqt*sign_R 79 | adzdr = np.sqrt(dzdr**2 + 1) 80 | dzdr = dzdr/adzdr 81 | if returnformat=='seqrt': 82 | N = np.column_stack((-dzdr*np.sin(phi), -dzdr*np.cos(phi), 1/adzdr)) 83 | else: 84 | N = (dzdr*np.cos(phi), dzdr*np.sin(phi), 1./adzdr) 85 | #N = (1./adzdr, dzdr*np.cos(phi), dzdr*np.sin(phi)) 86 | return N 87 | 88 | """ conic """ 89 | 90 | # implement from asphere when/if needed 91 | 92 | """ polynomic """ 93 | 94 | # implement from asphere when/if needed 95 | 96 | """ asphere """ 97 | 98 | def get_dzdr_evenasphere(r, R, k, A): 99 | sqt = np.sqrt(1 - (1+k)*r**2/R**2) 100 | term1 = 2*r/R/(1 + sqt) # conic term - derivative of nominator 101 | term2 = (1 + k) * r**3 # conic term - derivative of denominator pt1 102 | term2_div = sqt * R**3 * (1 + sqt)**2 # conic term - derivative of denominator pt2 103 | term3 = sum([2*(i+2)*A[i]*r**(2*(i+2) - 1) for i in range(len(A))]) # polynominal term 104 | return term1 + term2/term2_div + term3 105 | 106 | # def get_zN_asphere(r, phi, R, k, A, returnformat=''): 107 | # pass 108 | 109 | def get_z_evenasphere(r2, R, k, A): 110 | term1 = r2/R/(1 + np.sqrt(1 - (1+k)*r2/R**2)) # conic term 111 | term2 = sum([A[i]*r2**(i+2) for i in range(len(A))]) # polynominal term 112 | return term1 + term2 113 | 114 | def get_N_evenasphere(r, phi, R, k, A, returnformat=''): 115 | dzdr = get_dzdr_evenasphere(r, R, k, A) 116 | adzdr = np.sqrt(dzdr**2 + 1) 117 | dzdr = dzdr/adzdr 118 | if returnformat=='seqrt': 119 | N = np.column_stack((-dzdr*np.cos(phi), -dzdr*np.sin(phi), 1/adzdr)) 120 | else: 121 | N = (dzdr*np.cos(phi), dzdr*np.sin(phi), 1./adzdr) 122 | # N = (1./adzdr, dzdr*np.sin(phi), dzdr*np.cos(phi)) 123 | return N -------------------------------------------------------------------------------- /surface/rim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def get_ringnormals(N, dshape=False): 23 | normals = [] 24 | 25 | minb = 0 26 | maxb = 2*np.pi 27 | if dshape: 28 | minb = -np.pi/2 29 | maxb = np.pi*N/(N-1) 30 | 31 | for j in range(N): 32 | b = maxb*j/N + minb 33 | normals.append((np.cos(b), np.sin(b), 0)) 34 | # normals.append((0., np.sin(b), np.cos(b))) 35 | 36 | return normals 37 | 38 | def get_sqringnormals(N1, N2, dshape=False): 39 | normals = [] 40 | 41 | """Vertices added in same order as for sqspherical, but outline only""" 42 | # left side 43 | for i in range(N2): 44 | normals.append((-1, 0, 0)) 45 | # right side 46 | for i in range(N2): 47 | normals.append((1, 0, 0)) 48 | # bottom and top, alternating 49 | for i in range(N1 - 2): 50 | normals.append((0, -1, 0)) 51 | normals.append((0, 1, 0)) 52 | 53 | return normals -------------------------------------------------------------------------------- /surface/sagsurface.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .radialprofiles import get_zN_spherical, get_z_spherical, get_N_spherical, get_z_evenasphere, get_N_evenasphere 4 | # from .spherical import get_z_spherical, get_N_spherical 5 | from .aspheric import _check_k # get_z_asphere, get_N_asphere, 6 | from .toric import get_zN_toric 7 | from .cylindrical import get_zN_cylindrical, get_zN_acylindrical 8 | 9 | 10 | def add_sagsurface_circular(R, k, A, lrad, N1, N2, 11 | surftype='aspheric', 12 | rad2=None, k2=None, A2=None, 13 | surf_rotation=0, 14 | zadd=0, nVerts=0, dshape=False, lrad_ext=0): 15 | verts = [] 16 | faces = [] 17 | normals = [] 18 | 19 | make_triface = surftype in ['cylindrical', 'acylindrical', 'toric'] 20 | 21 | minb = 0 22 | maxb = 2*np.pi 23 | if dshape: 24 | minb = -np.pi/2 25 | maxb = np.pi*N2/(N2-1) 26 | 27 | if surftype=='aspheric': 28 | k = _check_k(k, R, lrad) 29 | 30 | verts.append([0, 0, -zadd]) 31 | normals.append((0, 0, 1)) 32 | """outer loop""" 33 | for i in range(N1 - (lrad_ext > lrad)): 34 | r = lrad*(i+1)/(N1 - (lrad_ext > lrad)) 35 | # for rotational surfaces, get vertex in outer loop for efficiency 36 | if surftype == 'spherical': 37 | z = get_z_spherical(r, R) 38 | elif surftype == 'aspheric': 39 | z = get_z_evenasphere(r**2, R, k, A) 40 | """inner loop""" 41 | for j in range(N2): 42 | phi = maxb*j/N2 + minb 43 | x = r*np.cos(phi) 44 | y = r*np.sin(phi) 45 | # for rotational surfaces, get only normal, else get both vertex and normal 46 | if surftype == 'spherical': 47 | N = get_N_spherical(r, phi, R) 48 | elif surftype == 'aspheric': 49 | N = get_N_evenasphere(r, phi, R, k, A) 50 | elif surftype == 'cylindrical': 51 | z, N = get_zN_cylindrical(x, y, R, surf_rotation=surf_rotation) 52 | elif surftype == 'acylindrical': 53 | z, N = get_zN_acylindrical(x, y, R, k, A, surf_rotation=surf_rotation) 54 | elif surftype == 'toric': 55 | z, N = get_zN_toric(x, y, R, rad2, surf_rotation=surf_rotation) 56 | verts.append([x, y, -z - zadd]) 57 | normals.append(N) 58 | if dshape and j==N2-1: 59 | pass 60 | elif i==0: 61 | # between center vertex and first ring, there are always triangles 62 | fi1 = nVerts 63 | fi2 = fi1+((j+1)%N2+1) 64 | fi3 = fi1+(j+1) 65 | faces.append([fi3,fi2,fi1]) 66 | else: 67 | fi1 = nVerts+(j+1+i*N2) 68 | fi2 = nVerts+((j+1)%N2+1+i*N2) 69 | fi3 = fi2-N2 70 | fi4 = fi1-N2 71 | if make_triface: 72 | # for surfaces without rotational symmetry, need tris 73 | faces.append([fi2, fi3, fi4]) 74 | faces.append([fi1, fi2, fi4]) 75 | else: 76 | # for surfaces with rotational symmetry, can make quads 77 | faces.append([fi1,fi2,fi3,fi4]) 78 | 79 | # 80 | # DO NOT ADD CODE HERE WITHOUT OBSERVING THE NOTE BELOW 81 | # 82 | 83 | # if there is flat annulus 84 | # ATTENTION: this uses values from the last loop above. 85 | # Take care not to overwrite if code is added. 86 | if lrad_ext > lrad: 87 | i = N1 - 2 88 | r = lrad_ext 89 | for j in range(N2): 90 | phi = maxb*j/N2 + minb 91 | x = r*np.cos(phi) 92 | y = r*np.sin(phi) 93 | verts.append([x, y, -z - zadd]) 94 | normals.append((0, 0, 1)) 95 | if dshape and j==N2-1: 96 | pass 97 | else: 98 | fi1 = nVerts + (j+i*N2+1)+N2 99 | fi2 = nVerts + ((j+1)%N2+i*N2+1) + N2 100 | fi3 = fi2 - N2 101 | fi4 = fi1 - N2 102 | faces.append([fi1,fi2,fi3,fi4]) 103 | 104 | return verts, faces, normals 105 | 106 | def add_sagsurface_rectangular(R, k, A, lwidth, N1, N2, 107 | surftype='aspheric', 108 | rad2=None, k2=None, A2=None, 109 | surf_rotation=0, 110 | zadd=0, nVerts=0, dshape=False, lrad_ext=0): 111 | verts = [] 112 | faces = [] 113 | vertquads = [] 114 | normals = [] 115 | vo = [] # outline verts 116 | 117 | if surftype=='aspheric': 118 | testrad = lwidth*np.sqrt(2) 119 | k = _check_k(k, R, testrad) 120 | 121 | for i in range(N1): 122 | if dshape: 123 | x = (i/(N1 - 1))*lwidth 124 | else: 125 | x = 2*(i/(N1 - 1) - 0.5)*lwidth 126 | for j in range(N2): 127 | y = 2*(j/(N2 - 1) - 0.5)*lwidth 128 | r = np.sqrt(x**2 + y**2) 129 | phi = np.arctan2(y, x) 130 | if surftype == 'spherical': 131 | z, N = get_zN_spherical(r, phi, R) 132 | # z = get_z_spherical(r, R) 133 | # N = get_N_spherical(r, phi, R) 134 | elif surftype == 'aspheric': 135 | z = get_z_evenasphere(r**2, R, k, A) 136 | N = get_N_evenasphere(r, phi, R, k, A) 137 | elif surftype == 'cylindrical': 138 | z, N = get_zN_cylindrical(x, y, R, surf_rotation=surf_rotation) 139 | elif surftype == 'acylindrical': 140 | z, N = get_zN_acylindrical(x, y, R, k, A, surf_rotation=surf_rotation) 141 | elif surftype == 'toric': 142 | z, N = get_zN_toric(x, y, R, rad2, surf_rotation=surf_rotation) 143 | verts.append([x, y, -z-zadd]) 144 | if i == 0 or i == N1-1: 145 | vo.append(verts[-1]) 146 | elif j == 0 or j == N2-1: 147 | vo.append(verts[-1]) 148 | normals.append(N) 149 | ang = np.arctan2(y + lwidth/(2*N2-2), x + lwidth/(2*N1-2)) 150 | vertquads.append(ang/np.pi*2) 151 | 152 | for i in range(N1-1): 153 | for j in range(N2-1): 154 | f1 = nVerts + j + 1 + N2*i 155 | f2 = nVerts + j + N2*i 156 | f3 = nVerts + j + N2*(i+1) 157 | f4 = nVerts + j + 1 + N2*(i+1) 158 | if vertquads[j+i*N2] >= 1: 159 | faces.append([f1,f2,f4]) 160 | faces.append([f2,f3,f4]) 161 | elif vertquads[j+i*N2] >= 0: 162 | faces.append([f1,f2,f3]) 163 | faces.append([f1,f3,f4]) 164 | elif vertquads[j+i*N2] >= -1: 165 | faces.append([f1,f2,f4]) 166 | faces.append([f2,f3,f4]) 167 | elif vertquads[j+i*N2] >= -2: 168 | faces.append([f1,f2,f3]) 169 | faces.append([f1,f3,f4]) 170 | else: 171 | faces.append([f1,f2,f3,f4]) 172 | 173 | # reorder outline verts to go around 174 | if not dshape: 175 | vo_row1 = vo[:N1] 176 | vo_row2 = vo[-N1:] 177 | vo_col1 = vo[N1:-N1][::2] 178 | vo_col2 = vo[N1:-N1][1::2] 179 | vo = vo_row1[::-1] + vo_col1 + vo_row2+ vo_col2[::-1] 180 | 181 | return verts, faces, normals, vo -------------------------------------------------------------------------------- /surface/spherical.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def add_spherical_surface(rad, lrad, N1, N2, zadd=0, nVerts=0, cylinderaxis=None, 23 | hole=False, hrad=0, dshape=False, lrad_ext=0): 24 | verts = [] 25 | faces = [] 26 | normals = [] 27 | 28 | minb = 0 29 | maxb = 2*np.pi 30 | if dshape: 31 | minb = -np.pi/2 32 | maxb = np.pi*N2/(N2-1) 33 | 34 | sig = 1 35 | if rad < 0: 36 | sig = -1 37 | nhole = not hole 38 | rad = np.abs(rad) 39 | ang = np.arcsin(lrad/rad) 40 | 41 | # central vertex only without hole 42 | if not hole: 43 | verts.append([0, 0, -zadd]) 44 | normals.append((0, 0, 1)) 45 | hang = 0 # angle where first ring is placed 46 | else: 47 | hang = np.arcsin(hrad/rad) 48 | 49 | # create rings 50 | for i in range(N1 - (lrad_ext > lrad)): 51 | a = hang + (ang-hang)*(i+nhole)/(N1 - hole - (lrad_ext > lrad)) 52 | r0 = rad*np.sin(a) 53 | for j in range(N2): 54 | b = maxb*j/N2 + minb 55 | x = r0*np.cos(b) 56 | y = r0*np.sin(b) 57 | if cylinderaxis == 'X': 58 | r = x 59 | elif cylinderaxis == 'Y': 60 | r = y 61 | else: 62 | r = r0 63 | z = rad-np.sqrt(rad**2-r**2) 64 | verts.append([x, y, -z*sig-zadd]) 65 | normals.append((sig*np.sin(a)*np.cos(b), 66 | sig*np.sin(a)*np.sin(b), 67 | np.cos(a))) 68 | if dshape and j==N2-1: 69 | pass 70 | elif i == 0: 71 | if not hole: 72 | fi1 = nVerts 73 | fi2 = fi1 + ((j+1)%N2+1) 74 | fi3 = fi1 + (j+1) 75 | faces.append([fi3,fi2,fi1]) 76 | else: 77 | fi1 = nVerts + j+nhole+i*N2 78 | fi2 = nVerts + (j+1)%N2+nhole+i*N2 79 | fi3 = fi2 - N2 80 | fi4 = fi1 - N2 81 | if cylinderaxis is not None: 82 | # in this case faces must be added as tris because 4 vertices will not lie in a plane in general 83 | faces.append([fi2, fi3, fi4]) 84 | faces.append([fi1, fi2, fi4]) 85 | else: 86 | faces.append([fi1,fi2,fi3,fi4]) 87 | #faces.append([fi4,fi3,fi2,fi1]) 88 | 89 | #if there is flat annulus, add the outer ring 90 | if lrad_ext > lrad: 91 | i = N1 - 2 92 | r = lrad_ext 93 | for j in range(N2): 94 | b = maxb*j/N2 + minb 95 | verts.append([r*np.cos(b), r*np.sin(b), -z*sig - zadd]) 96 | normals.append((0, 0, 1)) 97 | if dshape and j==N2-1: 98 | pass 99 | else: 100 | fi1 = nVerts + (j+nhole+i*N2)+N2 101 | fi2 = nVerts + ((j+1)%N2+nhole+i*N2) + N2 102 | fi3 = fi2 - N2 103 | fi4 = fi1 - N2 104 | faces.append([fi1,fi2,fi3,fi4]) 105 | #faces.append([fi4,fi3,fi2,fi1]) 106 | 107 | return verts, faces, normals 108 | 109 | 110 | def add_sqspherical_surface(rad, lwidth, N1, N2, zadd=0, nVerts=0, 111 | cylinderaxis=None, dshape=False, lwidth_ext=0): 112 | """ 113 | zadd has to be set for second surface (only) 114 | """ 115 | 116 | verts = [] 117 | faces = [] 118 | normals = [] 119 | vertquads = [] 120 | vo = [] # outline verts 121 | 122 | sig = 1 123 | if rad < 0: 124 | sig = -1 125 | rad = np.abs(rad) 126 | 127 | hasflangeY = (lwidth_ext > lwidth) and cylinderaxis == 'X' 128 | hasflangeZ = (lwidth_ext > lwidth) and cylinderaxis == 'Y' 129 | 130 | if cylinderaxis== 'X': 131 | lwidthY = lwidth 132 | lwidthZ = lwidth_ext 133 | elif cylinderaxis== 'Y': 134 | lwidthY = lwidth_ext 135 | lwidthZ = lwidth 136 | else: 137 | lwidthY = lwidth 138 | lwidthZ = lwidth 139 | 140 | for i in range(N1): 141 | if dshape: 142 | if i==N1-1 and hasflangeY: 143 | y = lwidth_ext 144 | else: 145 | y = (i/(N1 - 1))*lwidthY 146 | else: 147 | if i==0 and hasflangeY: 148 | y = -lwidth_ext 149 | elif i==N1-1 and hasflangeY: 150 | y = lwidth_ext 151 | else: 152 | y = 2*(i/(N1 - 1) - 0.5)*lwidthY 153 | for j in range(N2): 154 | is_flangevert = False 155 | if j==0 and hasflangeZ: 156 | z = -lwidthZ 157 | elif j==N2-1 and hasflangeZ: 158 | z = lwidthZ 159 | else: 160 | z = 2*(j/(N2 - 1) - 0.5)*lwidthZ 161 | if cylinderaxis == 'X': 162 | if i==0 and hasflangeY and not dshape: 163 | r = 2*((i+1)/(N1 - 1) - 0.5)*lwidthY 164 | is_flangevert = True 165 | elif i==N1-1 and hasflangeY: 166 | r = 2*((i-1)/(N1 - 1) - 0.5)*lwidthY 167 | is_flangevert = True 168 | else: 169 | r = y 170 | elif cylinderaxis == 'Y': 171 | if j==0 and hasflangeZ: 172 | r = 2*((j+1)/(N2 - 1) - 0.5)*lwidthZ 173 | is_flangevert = True 174 | elif j==N2-1 and hasflangeZ: 175 | r = 2*((j-1)/(N2 - 1) - 0.5)*lwidthZ 176 | is_flangevert = True 177 | else: 178 | r = z 179 | else: 180 | r = np.sqrt(y**2 + z**2) 181 | x = rad - np.sqrt(rad**2 - r**2) 182 | verts.append([y, z, -x*sig - zadd]) 183 | if i == 0 or i == N1-1: 184 | vo.append(verts[-1]) 185 | elif j == 0 or j == N2-1: 186 | vo.append(verts[-1]) 187 | ang = np.arctan2(z + lwidth_ext/(N2-1), y + lwidth_ext/(N1-1)) 188 | # Normals 189 | if is_flangevert: 190 | normals.append((0, 0, 1)) 191 | else: 192 | a = np.arcsin(r/rad) 193 | if cylinderaxis == 'X': 194 | b = np.pi/2 195 | elif cylinderaxis == 'Y': 196 | b = 0 197 | else: 198 | b = np.arctan2(y, z) 199 | normals.append((sig*np.sin(a)*np.sin(b), 200 | sig*np.sin(a)*np.cos(b), 201 | np.cos(a))) 202 | # marker for face generation 203 | cond1 = N2%2 == 0 204 | cond2 = j==int(N2/2 - 1) or (i==int(N1/2 - 1) and not dshape) 205 | if cond1 and cond2: 206 | vertquads.append(-99) 207 | else: 208 | vertquads.append(ang/np.pi*2) 209 | 210 | # add the faces of the spherical portion 211 | for i in range(N1-1): 212 | for j in range(N2-1): 213 | f1 = nVerts + j + 1 + N2*i 214 | f2 = nVerts + j + N2*i 215 | f3 = nVerts + j + N2*(i+1) 216 | f4 = nVerts + j + 1 + N2*(i+1) 217 | if vertquads[j+i*N2] >= 1: 218 | faces.append([f1,f2,f4]) 219 | faces.append([f2,f3,f4]) 220 | elif vertquads[j+i*N2] >= 0: 221 | faces.append([f1,f2,f3]) 222 | faces.append([f1,f3,f4]) 223 | elif vertquads[j+i*N2] >= -1: 224 | faces.append([f1,f2,f4]) 225 | faces.append([f2,f3,f4]) 226 | elif vertquads[j+i*N2] >= -2: 227 | faces.append([f1,f2,f3]) 228 | faces.append([f1,f3,f4]) 229 | else: 230 | faces.append([f1,f2,f3,f4]) 231 | 232 | # reorder outline verts to go around 233 | if not dshape: 234 | vo_row1 = vo[:N1] 235 | vo_row2 = vo[-N1:] 236 | vo_col1 = vo[N1:-N1][::2] 237 | vo_col2 = vo[N1:-N1][1::2] 238 | vo = vo_row1[::-1] + vo_col1 + vo_row2+ vo_col2[::-1] 239 | 240 | return verts, faces, normals, vo -------------------------------------------------------------------------------- /surface/surfaceutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | from ..utils.geometry import get_rotmat_z 22 | 23 | def get_N1_sqsurface(N2, dshape): 24 | # passing N2 and calculating N1, not the other way, for historical reasons 25 | if dshape: 26 | N1 = N2//2 + 1 27 | else: 28 | N1 = N2 29 | return N1 30 | 31 | def get_xy_rotatedsurface(x, y, surf_rotation): 32 | """ 33 | This function takes x and y coordiantes as input and rotates them around the z-axis, 34 | one application being rotated surfaces such as cylinder lenses. 35 | """ 36 | input_is_array = isinstance(x, np.ndarray) 37 | # rotate the input coordiantes by -surf_rotation to bring them into the local coordinate system 38 | # R_z_pos = get_rotmat_z(surf_rotation) 39 | R_z_neg = get_rotmat_z(-surf_rotation) 40 | 41 | if input_is_array: 42 | arraydim = len(x.shape) 43 | if arraydim > 1: 44 | scp = x.shape 45 | x = x.ravel() 46 | y = y.ravel() 47 | coords_in = np.column_stack((x, y, np.zeros(x.shape[0]))) 48 | coords_rot = np.matmul(R_z_neg, coords_in.T).T 49 | x = coords_rot[:,0] 50 | y = coords_rot[:,1] 51 | if arraydim > 1: 52 | x = x.reshape(scp) 53 | y = y.reshape(scp) 54 | else: 55 | coords_in = np.array([x, y, 0]) 56 | coords_rot = np.matmul(R_z_neg, coords_in) 57 | x = coords_rot[0] 58 | y = coords_rot[1] 59 | 60 | return x, y -------------------------------------------------------------------------------- /surface/toric.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2025, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | from .surfaceutils import get_xy_rotatedsurface 23 | from ..utils.geometry import rotate_vector_x, rotate_vector_z 24 | 25 | def get_zN_toric(x, y, R, r, surf_rotation=0, coord_input='original', returnformat=''): 26 | if coord_input == 'original' and not surf_rotation == 0: 27 | # rotate the coordinates to align with the cylidner axis 28 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 29 | # Here, the functions for z and N are combined for numerical efficiency 30 | R_neg = R < 0 31 | r_neg = r < 0 32 | sign_R = 1 - 2*R_neg 33 | sign_r = 1 - 2*r_neg 34 | sign_prod = sign_R*sign_r 35 | # sign_both = 1 - 2*(R_neg and r_neg) 36 | r = abs(r) 37 | R = abs(R) 38 | R_T = R - r*sign_prod 39 | 40 | # get z 41 | sqrt_1 = sign_prod*np.sqrt(r**2 - y**2) 42 | sqrt_2 = np.sqrt((R_T + sqrt_1)**2 - x**2) 43 | z = (R - sqrt_2)*sign_R 44 | 45 | # get N 46 | inv_sqrt_2 = 1./sqrt_2 47 | dfdy = sign_R*y*(R_T + sqrt_1)/sqrt_1*inv_sqrt_2 48 | dfdx = sign_R*x*inv_sqrt_2 49 | inv_absval = 1./np.sqrt(dfdx**2 + dfdy**2 + 1) 50 | m = inv_absval # multiplier for all values 51 | if returnformat=='seqrt': 52 | N = np.column_stack((-dfdx*m, -dfdy*m, m)) 53 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 54 | else: 55 | N = (dfdx*m, dfdy*m, m) 56 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 57 | #N = (m, dfdx*m, dfdy*m) 58 | #N = rotate_vector_x(N, surf_rotation=surf_rotation) 59 | return z, N 60 | 61 | def get_z_toric(x, y, R, r, surf_rotation=0, coord_input='original'): 62 | if coord_input == 'original' and not surf_rotation == 0: 63 | # rotate the coordinates to align with the cylidner axis 64 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 65 | R_neg = R < 0 66 | r_neg = r < 0 67 | sign_R = 1 - 2*R_neg 68 | sign_r = 1 - 2*r_neg 69 | sign_prod = sign_R*sign_r 70 | # sign_both = 1 - 2*(R_neg and r_neg) 71 | r = abs(r) 72 | R = abs(R) 73 | R_T = R - r*sign_prod 74 | 75 | # get z 76 | sqrt_1 = sign_prod*np.sqrt(r**2 - y**2) 77 | sqrt_2 = np.sqrt((R_T + sqrt_1)**2 - x**2) 78 | return (R - sqrt_2)*sign_R 79 | 80 | def get_N_toric(x, y, R, r, surf_rotation=0, coord_input='original', returnformat=''): 81 | if coord_input == 'original' and not surf_rotation == 0: 82 | # rotate the coordinates to align with the cylidner axis 83 | x, y = get_xy_rotatedsurface(x, y, surf_rotation) 84 | R_neg = R < 0 85 | r_neg = r < 0 86 | sign_R = 1 - 2*R_neg 87 | sign_r = 1 - 2*r_neg 88 | sign_prod = sign_R*sign_r 89 | # sign_both = 1 - 2*(R_neg and r_neg) 90 | r = abs(r) 91 | R = abs(R) 92 | R_T = R - r*sign_prod 93 | 94 | sqrt_1 = sign_prod*np.sqrt(r**2 - y**2) 95 | sqrt_2 = np.sqrt((R_T + sqrt_1)**2 - x**2) 96 | 97 | inv_sqrt_2 = 1./sqrt_2 98 | dfdy = sign_R*y*(R_T + sqrt_1)/sqrt_1*inv_sqrt_2 99 | dfdx = sign_R*x*inv_sqrt_2 100 | inv_absval = 1./np.sqrt(dfdx**2 + dfdy**2 + 1) 101 | m = inv_absval # multiplier for all values 102 | if returnformat=='seqrt': 103 | N = np.column_stack((-dfdx*m, -dfdy*m, m)) 104 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 105 | else: 106 | N = (dfdx*m, dfdy*m, m) 107 | N = rotate_vector_z(N, surf_rotation=surf_rotation) 108 | #N = (m, dfdx*m, dfdy*m) 109 | #N = rotate_vector_x(N, surf_rotation=surf_rotation) 110 | return N -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | from .check_surface import * 21 | 22 | DEBUGPRINT = False 23 | def debugprint(*messages): 24 | if len(messages) == 0: messages = '' 25 | if DEBUGPRINT: print(*messages) -------------------------------------------------------------------------------- /utils/check_surface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | def surftype_zmx2ltype(rx, ry, kx, ky, Ax, Ay): 23 | """Determine the ltype parameter for the add_lens() function (Blender geometry import)""" 24 | surf_subtype1 = get_surface_subtype(rx, kx, Ax) 25 | surf_subtype2 = get_surface_subtype(ry, ky, Ay) 26 | # evalaute some bools for better readability below 27 | s1None = surf_subtype1 == 'surf_subtype_None' 28 | s2None = surf_subtype2 == 'surf_subtype_None' 29 | s1flat = surf_subtype1 == 'flat' 30 | s2flat = surf_subtype2 == 'flat' 31 | 32 | # unset None for equality checking 33 | if rx is None: rx = 0 34 | if ry is None: ry = 0 35 | if kx is None: kx = 0 36 | if ky is None: ky = 0 37 | if np.all(np.array(Ax) == None): Ax = [0] 38 | if np.all(np.array(Ay) == None): Ay = [0] 39 | allequal = rx == ry and kx == ky and Ax == Ay 40 | 41 | # case : bad input 42 | if s1None and s2None: 43 | return 'surftype_None' 44 | 45 | # case : flat (return rotational as default) 46 | elif (s1None and s2flat) or (s2None and s1flat) or (s1flat and s2flat): 47 | return 'flat' 48 | 49 | # case : rotational 50 | elif (not s1None and s2None) or (s1None and not s2None) or allequal: 51 | return 'rotational' 52 | 53 | # case : cylindricX 54 | elif not s1None and not s1flat and s2flat: 55 | return 'cylindricX' 56 | 57 | # case : cylindricY 58 | elif not s2None and not s2flat and s1flat: 59 | return 'cylindricY' 60 | 61 | # case : toric 62 | elif (not s1None and not s1flat) and (not s2None and not s2flat): 63 | return 'toric' 64 | 65 | # else: not implemented cases (return rotational as default) 66 | else: 67 | return 'rotational' 68 | 69 | 70 | def surftype_Lens2Element(ltype, surf_subtype): 71 | if surf_subtype == 'flat': return 'flat' 72 | key = '_'.join((ltype, surf_subtype)) 73 | LOOKUP = {'rotational_spherical': 'spherical', 74 | 'rotational_conic': 'conical', 75 | 'rotational_polynominal': 'polynominal', 76 | 'rotational_aspheric': 'aspheric', 77 | 'cylindrical_spherical': 'cylindrical', 78 | 'cylindrical_conic': 'conicylindrical', 79 | 'cylindrical_polynominal': 'polycylindrical', 80 | 'cylindrical_aspheric': 'acylindrical', 81 | 'toric_spherical': 'toric'} 82 | """ # not yet implemented 83 | '', 'conitoric', 84 | '', 'polytoric', 85 | '', 'atoric',} 86 | """ 87 | return LOOKUP[key] 88 | 89 | def get_surface_subtype(r, k, A): 90 | """determine the type of surface w.r.t. the intersection algorithms:""" 91 | if r is None and np.all(np.array(A) == None): 92 | return 'surf_subtype_None' 93 | 94 | hasrad = r != 0 and r is not None 95 | hasconic = k != 0 and k is not None 96 | haspoly = not (np.all(np.array(A) == 0) or np.all(np.array(A) == None) or A is None) 97 | 98 | if not hasrad and not haspoly: 99 | surf_subtype = 'flat' 100 | elif not hasrad and haspoly: 101 | surf_subtype = 'flat' # 'polynominal' # pure polynominal not yet supported 102 | elif not hasconic and not haspoly: 103 | surf_subtype = 'spherical' 104 | #elif hasrad and not haspoly and k == -1: 105 | # surf_subtype = 'parabolic' 106 | elif hasconic and not haspoly: 107 | surf_subtype = 'conic' 108 | else: 109 | surf_subtype = 'aspheric' 110 | 111 | return surf_subtype 112 | 113 | def check_surface(lrad, flrad, RX, kX, AX, RY, kY, AY, surftype, squarelens): 114 | 115 | # special case of flat surface will be used in multiple cases 116 | returnvalues_flat = (lrad, 0, 0, 'flat', 'flat', 'flat', 0, 0, [0], 0) 117 | 118 | if kX is None: kX = 0 119 | if kY is None: kY = 0 120 | if AX is None or AX == [] or np.all(np.array(AX) == [None]): AX = [0] 121 | if AY is None or AY == [] or np.all(np.array(AY) == [None]): AY = [0] 122 | surf_subtype_X = get_surface_subtype(RX, kX, AX) 123 | surf_subtype_Y = get_surface_subtype(RY, kY, AY) 124 | 125 | # No flange unless explicitly confirmed 126 | hasfl = 0 127 | lrad_surf = lrad 128 | # same for surface rotation offset 129 | dSurfrot = 0 130 | 131 | """ Case 1: Flat """ 132 | if surftype == 'flat' or (surf_subtype_X == 'flat' and surf_subtype_Y == 'flat'): 133 | return returnvalues_flat 134 | 135 | """ Case 2: Rotational """ 136 | # all Y values are ignored for this case 137 | if surftype == 'rotational': 138 | if surf_subtype_X == 'flat': 139 | return returnvalues_flat 140 | if not squarelens: 141 | if flrad > 0.99*lrad: # maximum flange width 142 | return returnvalues_flat 143 | hasfl = flrad > 0.01*lrad # minimum flange width 144 | lrad_surf = lrad - flrad if hasfl else lrad 145 | if kX <= -1: # for k<-1, the argument in the sqrt of the conic term is always positive, i.e. well-defined 146 | pass 147 | elif lrad_surf > abs(RX)/np.sqrt(1+kX): # automatic flange to maximum of well-defined radius 148 | lrad_surf = 0.9999*abs(RX)/np.sqrt(1+kX) 149 | flrad = lrad - lrad_surf 150 | hasfl = 1 151 | 152 | """ Case 3: Cylindrical """ 153 | # all Y values are ignored for this case 154 | if surftype == 'cylindrical': 155 | if surf_subtype_X == 'flat': 156 | return returnvalues_flat 157 | if squarelens: 158 | if flrad > 0.99*lrad: # maximum flange width 159 | return returnvalues_flat 160 | hasfl = flrad > 0.01*lrad # minimum flange width 161 | lrad_surf = lrad - flrad if hasfl else lrad 162 | if kX <= -1: # for k<-1, the argument in the sqrt of the conic term is always positive, i.e. well-defined 163 | pass 164 | elif lrad_surf > abs(RX)/np.sqrt(1+kX): # automatic flange to maximum of well-defined radius 165 | lrad_surf = 0.9999*abs(RX)/np.sqrt(1+kX) 166 | flrad = lrad - lrad_surf 167 | hasfl = 1 168 | 169 | """ Case 4: Toric """ 170 | if surftype == 'toric': 171 | if surf_subtype_X == 'flat' and surf_subtype_Y == 'flat': 172 | return returnvalues_flat 173 | if surf_subtype_Y == 'flat' and not surf_subtype_X == 'flat': 174 | # equivalent to a cylinder, X-axis already valid 175 | surftype = 'cylindrical' 176 | elif surf_subtype_X == 'flat' and not surf_subtype_Y == 'flat': 177 | # equivalent to a cylinder, flip parameters 178 | surftype = 'cylindrical' 179 | surf_subtype_X, surf_subtype_Y = surf_subtype_Y, surf_subtype_X 180 | RX, kX, AX, RY, kY, AY = RY, kY, AY, RX, kX, AX 181 | dSurfrot = np.pi/2 182 | 183 | """ Default return case """ 184 | return lrad_surf, hasfl, flrad, surftype, surf_subtype_X, surf_subtype_Y, RX, kX, AX, dSurfrot -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | Fraunhofer_map = {"i": 365.0146, 21 | "h": 404.6561, 22 | "g": 435.8343, 23 | "F'": 479.9914, 24 | "F": 486.1327, 25 | "e": 546.0740, 26 | "d": 587.5618, 27 | "D": 589.2938, 28 | "C'": 643.8469, 29 | "C": 656.2725, 30 | "r": 706.5188, 31 | "A'": 768.2000, 32 | "s": 852.1100, 33 | "t": 1013.9800,} 34 | 35 | digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] -------------------------------------------------------------------------------- /utils/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | import numpy as np 21 | 22 | """ 23 | To apply rotation matrix R to a vector V of shape (3,): 24 | np.matmul(R, V) 25 | 26 | to apply to a multiple vector U of shape (N, 3) 27 | np.matmul(R, U.T).T 28 | """ 29 | 30 | def get_rotmat_x(phi): 31 | cphi = np.cos(phi) 32 | sphi = np.sin(phi) 33 | R = [[1, 0, 0 ], 34 | [0, cphi, -sphi], 35 | [0, sphi, cphi ]] 36 | return np.array(R) 37 | 38 | def get_rotmat_y(phi): 39 | cphi = np.cos(phi) 40 | sphi = np.sin(phi) 41 | R = [[cphi, 0, sphi], 42 | [0, 1, 0 ], 43 | [-sphi, 0, cphi]] 44 | return np.array(R) 45 | 46 | def get_rotmat_z(phi): 47 | cphi = np.cos(phi) 48 | sphi = np.sin(phi) 49 | R = [[cphi, -sphi, 0], 50 | [sphi, cphi, 0], 51 | [0, 0, 1]] 52 | return np.array(R) 53 | 54 | def get_rotmat_axis(phi, a=[1,0,0]): 55 | a1, a2, a3 = a 56 | cphi = np.cos(phi) 57 | sphi = np.sin(phi) 58 | mcphi = 1 - cphi 59 | R11 = a1*a1*mcphi + cphi 60 | R12 = a1*a2*mcphi - a3*sphi 61 | R13 = a1*a3*mcphi + a2*sphi 62 | R21 = a1*a2*mcphi + a3*sphi 63 | R22 = a2*a2*mcphi + cphi 64 | R23 = a2*a3*mcphi - a1*sphi 65 | R31 = a1*a3*mcphi - a2*sphi 66 | R32 = a2*a3*mcphi + a1*sphi 67 | R33 = a3*a3*mcphi + cphi 68 | R = [[R11, R12, R13], 69 | [R21, R22, R23], 70 | [R31, R32, R33]] 71 | return np.array(R) 72 | 73 | def rotate_vector_x(V, surf_rotation): 74 | input_is_array = isinstance(V, np.ndarray) 75 | R_z = get_rotmat_x(surf_rotation) 76 | if input_is_array: 77 | V = np.matmul(R_z, V.T).T 78 | else: 79 | V = np.array(V) 80 | V = np.matmul(R_z, V) 81 | V = [V[0], V[1], V[2]] 82 | return V 83 | 84 | def rotate_vector_y(V, surf_rotation): 85 | input_is_array = isinstance(V, np.ndarray) 86 | R_z = get_rotmat_y(surf_rotation) 87 | if input_is_array: 88 | V = np.matmul(R_z, V.T).T 89 | else: 90 | V = np.array(V) 91 | V = np.matmul(R_z, V) 92 | V = [V[0], V[1], V[2]] 93 | return V 94 | 95 | def rotate_vector_z(V, surf_rotation): 96 | input_is_array = isinstance(V, np.ndarray) 97 | R_z = get_rotmat_z(surf_rotation) 98 | if input_is_array: 99 | V = np.matmul(R_z, V.T).T 100 | else: 101 | V = np.array(V) 102 | V = np.matmul(R_z, V) 103 | V = [V[0], V[1], V[2]] 104 | return V 105 | 106 | 107 | """ TEST """ 108 | 109 | if __name__ == '__main__': 110 | print() 111 | testvector = np.arange(3) 112 | print('Testvector:') 113 | print(testvector) 114 | print() 115 | R = get_rotmat_z(np.pi/4) 116 | print('Rotmat:') 117 | print(R) 118 | print() 119 | result = np.matmul(R, testvector) 120 | print('Result:') 121 | print(result) 122 | print() 123 | print('-'*60) 124 | print() 125 | testvector = np.arange(5*3).reshape((5,3)) 126 | print('Testvector (long):') 127 | print(testvector) 128 | print() 129 | R = get_rotmat_z(np.pi/4) 130 | print('Rotmat:') 131 | print(R) 132 | print() 133 | result = np.matmul(R, testvector.T).T 134 | print('Result:') 135 | print(result) 136 | print() 137 | -------------------------------------------------------------------------------- /utils/lens_math.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ 19 | 20 | def f_lensmaker(r1,r2,n, d): 21 | if r1 == 0: 22 | invr1 = 0 23 | else: 24 | invr1 = 1./r1 25 | 26 | if r2 == 0: 27 | invr2 = 0 28 | else: 29 | invr2 = 1./r2 30 | 31 | if r1 == 0 or r2 == 0: 32 | invr1r2 = 0 33 | else: 34 | invr1r2 = 1./(r1*r2) 35 | 36 | dummy = (n-1) * (invr1 - invr2 + (n-1)*d*invr1r2/n) 37 | 38 | if dummy == 0: 39 | invdummy = 0 40 | else: 41 | invdummy = 1./dummy 42 | 43 | return invdummy 44 | 45 | def petzvalrad_surfaces(ns, rs): 46 | prods = [] 47 | for i in range(len(ns)-1): 48 | nom = ns[i+1] - ns[i] 49 | den = ns[i]*ns[i+1]*rs[i] 50 | prods.append(nom/den) 51 | psum = ns[-1] * sum(prods) 52 | return -1./psum 53 | 54 | def petzvalrad_thinlens(ns, fs): 55 | prods = [1/(x*y) for x,y in zip(ns, fs)] 56 | psum = sum(prods) 57 | return 1./psum -------------------------------------------------------------------------------- /utils/paraxial/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2019-2024, Johannes Hinrichs 3 | 4 | This file is part of OptiCore. 5 | 6 | OptiCore is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | OptiCore is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with OptiCore. If not, see . 18 | """ --------------------------------------------------------------------------------