├── .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 |
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 | """
--------------------------------------------------------------------------------