Custom DLLs in OpticStudio: An overview of user-defined surfaces, objects, and other DLL types

OpticStudio can employ a number of user-compiled Dynamic Link Libraries (DLLs) to extend and customize the functionality of the program. The use of DLLs allows for custom sequential surface and non-sequential object definitions, surface and bulk scatter models, gradient index profiles, diffraction models, and non-sequential source and physical optics beam definitions. This article provides an introduction to the various types of DLLs, how and when OpticStudio calls each DLL type, and the data structures passed back and forth between OpticStudio and a given user-defined DLL.

Authored By Zachary Derocher


While OpticStudio provides various design tools for commonly surface structures, scatter profiles, beam definitions, etc. some simulations require custom definitions. The most flexible option for defining custom optical properties is a user-defined DLL. Sequential mode allows custom Surface and POP beam profiles, while Non-Sequential allows for custom Object, Bulk Scatter, Diffractive, Gradient Index, Source, and Surface Scatter definitions. This permits the user to mathematically define a custom element of a design.

Here we discuss how OpticStudio employs DLLs, and introduce the data structures shared between OpticStudio and DLLs. For specific and well-commented example code, please refer to the appropriate sub-folder in the Zemax directory ...\Documents\Zemax\DLL\.


What’s the difference between a DLL and the API?

A DLL is a user-compiled program which is provided to OpticStudio, which OpticStudio then calls at its leisure (usually many times per analysis, i.e. once per ray, etc.). The user has little or no direct control over when OpticStudio actually runs the DLL. In the case of DLLs, OpticStudio is the client which calls the DLL (the server) and requests data to be returned. The data structure which is passed into the DLL (and subsequently passed back to OpticStudio from the DLL) is fixed, defined uniquely for each type of DLL. In sum, the DLL is called by OpticStudio to perform a very specific task, and is provided a fixed set of data which will be required for that calculation.

The API is also a feature which allows users to implement custom programs to extend the functionality of OpticStudio. But in the case of the API, the client-server roles are reversed. The API program is run (the user initiates the program by i.e. pressing a button in the UI), and the API program (client) calls upon OpticStudio (server) to perform some actions and/or return some data. While the data available to a DLL program is fixed and limited, an API application has access to nearly all aspects of the system prescription and OpticStudio tools. This makes it suitable for interacting at a higher level with OpticStudio; for instance, with the API one might request that OpticStudio run a PSF analysis and return the result, and perform some additional post-processing on the data. The API doesn’t allow custom definition of optical surfaces, etc., but may be used to create new tools, analyses, optimization operands, etc.


Sequential vs non-sequential DLLs

In Sequential Mode, the surface type controls nearly every aspect of the way that rays interact with an optical surface. The surface structure, any phase or diffraction profile, refractive effects due to index change and local slope, index gradient, and even (optionally) coating or attenuation factors can be controlled directly from the surface definition. There are a wide variety of native sequential surfaces, each with its own combination of these options. The sequential User Defined Surface (UDS) also controls all of these properties, and can use them to affect each ray traced through it. This means that in Sequential mode, the User Defined Surface is a very flexible, powerful DLL which controls almost all aspects of ray tracing.

On the other hand, in Non-Sequential Mode, many of these aspects of ray tracing are decoupled, each handled with a separate routine. Because of this, non-sequential DLLs are much more specialized in their functionality. A DLL might be specified to handle the diffractive effects of a given object face, but the object itself is native (i.e. a diffractive DLL applied to a native Diffraction Grating object). The opposite could also be true; a User Defined Object DLL (perhaps with some custom shape) could be specified to be diffractive, but the diffraction profile is defined natively. Because of this, there’s a wider variety of non-sequential DLL types, each with a more concise use-case and realm of influence in the optical model.

As can be seen in the sample code in …\Documents\Zemax\DLL\..., most of the program structures require two functions: one to define the user object (i.e. UserSourceDefinition, UserScatterDefinition, etc.), and a second to define the parameter names which are exposed to the user within the OpticStudio GUI (UserParamNames).


Sequential DLLs Non-Sequential DLLs
User Defined Surface User Defined Object
Beam Bulk Scatter
  Gradient Index
  Surface Scatter


Below, we look at each DLL type in more detail, in order to show the scope of its functionality.


Surface DLL (sequential User Defined Surface)

The User Defined Surface (UDS) is a sequential DLL which represents a sequential surface. Because the Sequential Mode surface properties such as diffraction, gradient index, etc. are all contained within the surface definition itself, the UDS DLL also allows for all of these functionalities (if required). The UDS DLL must be called by OpticStudio every time a ray is traced through the system, since OpticStudio will need to calculate the surface intercept, how it refracts through the surface, etc. As such, once added to the Lens Data Editor (LDE) the UDS DLL will be called many times whenever any analysis window is updated or ray trace is performed (via the Merit Function, solves, macros, etc.). It is also called when layouts are updated, not only for the layout ray traces, but for the surface profile drawing itself. The DLL is also called in several other cases as well, aside from ray-tracing and rendering. For instance, when the DLL is first loaded as a User Defined surface in the Lens Data Editor (LDE), OpticStudio must request the parameter column names to display in the LDE column headers. In all, there are 10 different possible modes in which OpticStudio might call the surface DLL, and which type of data is requested is determined by the Type value, in the Fixed Data structure passed to the DLL:


Type Value Data Requested of DLL by ZOS
0 General surface information (i.e. rotationally symmetric?)
1 Parameter column header names
2 Extra Data column header names (unused with OpticStudio)
3 Surface sag (for drawing in layouts)
4 Paraxial ray trace results (incident ray data is on a tangent plane, DLL must calculate output paraxial ray data)
5 Real ray trace results (incident ray data is on a tangent plane, DLL must calculate real intercept, path length added, and output ray data)
6 Only used with GRIN DLLs; index, index gradient data
7 Safe parameter data (default parameter values for LDE when loaded)
8 Memory/data initialization (first DLL call)
9 Memory release (final DLL call)


Surface DLLs are unique in that a separate header file (…\Documents\Zemax\DLL\Surfaces\usersurf.h) is used to define their data structure. In order to compile a User Defined Surface, the header file will have to be loaded in the project. There have been several implementations of the data structure passed between OpticStudio and the UDS, but it has always been two elements: Fixed_Data, and User_Data. The Fixed_Data (currently in it’s 5th implementation, Fixed_Data5) structure contains data which can’t be altered by the DLL (with a few exceptions), and includes things like the parameter values pulled from the LDE. The User_Data structure is what will be computed by the DLL and sent back to OpticStudio, including things like the output (i.e. refracted, diffracted, etc.) ray direction cosines and exact surface intercept coordinates (note that USER_DATA contains the incident ray coordinates and cosines, and these values will be overwritten by the DLL with the output ray data). Please see the header file to find full information about all the data passed between OpticStudio and the DLL.

As with most OpticStudio DLLs, the User Surface DLL will return a value of 0 if it executes without error, and -1 if an error occurs. Unlike other DLLs, OpticStudio can also interpret other return values as well. In particular, during real ray tracing, if the DLL returns a positive integer, OpticStudio interprets this as a ray miss error at that surface #; a negative integer indicates an error in refraction (i.e. TIR) at that surface number.

See the examples …\Documents\Zemax\DLL\Surfaces\us_stand.c, or …\us_itera.c, for an introductory example (these sample DLLs both represent the Standard Surface type, in slightly different ways). The sample code also contains a complete description of each DLL call type, and the data expected to be returned in each case. There are several dozen other samples in that directory as well, which may provide helpful starting points or guidance depending on the desired application.



Physical Optics DLL (sequential beam definition)

The Physical Optics DLL allows the user to create a custom initial beam definition, for use in the sequential Physical Optics Propagation (POP) analysis. The code is called upon initialization of the POP analysis, and is responsible for generating the initial beam complex amplitude profile (Ex and Ey real, imaginary as a function of (x,y) position), as well as the initial pilot beam parameters. In POP, the beam is represented as a grid of complex amplitude data points (the 'sample grid'). The DLL is responsible for calculating the initial values at each of the points in this grid. As such, when POP is initialized with a beam DLL, the DLL is called many times: once for each (x,y) position in the initial POP data grid to generate the full starting beam profile.

The data structure used for beam DLLs contains roughly 30 data items, some of which are used to pass data into the DLL, and some of which are used for calculated data to be sent back to OpticStudio. The DLL must populate data 10-13 with the Ex/Ey data for each (x,y) position on the initial data grid. In the special case that x=y=0, the DLL also must populate data 14-19 with the initial pilot beam data. Upon DLL initialization, OpticStudio first calls the UserParamNames function of the DLL, to determine the names of the parameters to be shown in the POP settings window. When the beam is to be launched, OpticStudio passes the relevant data into the DLL for use, such as the grid size/point spacing (so that the DLL knows the XY position of each grid point), the wavelength, the parameter values set by the user in the POP settings, etc. For information about the complete data structure shared between ZOS and the DLL, and for a demonstration on how to write a beam DLL, please refer to ...\Documents\Zemax\DLL\PhysicalOptics\beamsamp1.c, or any other sample file in that directory.



Object DLL (non-sequential User Defined Object)

In Non-Sequential Mode, custom objects can be defined using the User Defined Object (UDO) type. The UDO might be considered the most complex or labor-intensive non-sequential DLL, because a full 3D volume geometry must be defined, but it’s also relatively simple in terms of the data structure passed between applications. The DLL is called for object rendering (in Layouts), in which case a triangular-faceted approximation is use; the DLL is responsible for determining the number, vertex coordinates, and properties of each triangle in the faceted representation. The UDO DLL is also called during ray tracing, each time an individual ray is incident upon one of its surfaces. In this case, the facets are again used, but (optionally) only to provide a first-guess of ray coordinates/direction cosines to the DLL, which can then iterate to the exact intercept. 

The User Defined Object DLL is not itself responsible for computing the refraction/reflection/diffraction and the exit ray angles and energy; the DLL is only responsible for the exact ray intercept coordinates on the surface, and the surface normal. As such, for the purposes of the DLL, the only relevant ray data is XYZ coordinates and LMN direction cosines. For a given ray, the exact intercept coordinate and surface normal is sent back to OpticStudio, which performs the refraction (or reflection, diffraction, scattering, etc., based on the face and coating properties). Note that in the case of a diffractive surface, the DLL must inform OpticStudio that a given surface is diffractive via a simple boolean flag in the DLL. This 'modular' object definition allows for  complex diffraction properties to be defined using any diffraction model available (i.e. splitting by table, or even external diffraction DLLs). The same is true of gradient index, surface scatter, and bulk scatter models, just as is the case for native Non-Sequential objects. Because the actual ray tracing is handled by OpticStudio, the user can combine User Defined Object DLLs with custom bulk scatter, surface scatter, etc. DLLs!

Like the User Defined Surface, there are a few different requests that might be made of the DLL by OpticStudio. Firstly, there are two separate functions defined in the DLL, one for the object definition (UserObjectDefinition) and one simply for the parameter column names (UserParamNames). The latter is called only to generate the parameter column headers seen in the Non-Sequential Component Editor. For the object data, there are several subroutines defined; which type of data is requested of the DLL is determined based on a flag value, passed to the DLL as data[1]. In the case of the UDO, depending on the code value, the other data passed into the DLL will vary. A list of cases is shown here. For a more complete guide, please refer to the sample code ...\Documents\Zemax\DLL\Objects\HalfCylinder.c, or any other example file in that directory.


data[1] value ("code") Data Requested of DLL by ZOS
0 Basic data (solid/shell, diffractive?, number of facets)
1 Generate triangular facets for rough object description
2 Iterate ray to exact intercept coordinates from a given facet
3 Coating data
4 Safe data (initial NSCE parameter values)


Here we can see the faceted rendering (and also the initial approximated internal representation) of the Half Cylinder example UDO. As you can see in the image, the incident ray doesn't refract at the facet 'intercept', but the DLL iterates to the exact ray landing coordinate on the cylindrical (curved) face. The approximate representation, however, provides for much faster rendering and ray tracing speeds, as it allows OpticStudio to quickly determine whether the ray hits somewhere on the object or not before making the more rigorous iteration.



Bulk Scatter DLL (non-sequential)

In the bulk scatter DLL, OpticStudio passes to the user program information about the nominal (unscattered) ray path. The DLL must then decide whether or not the ray undergoes bulk scatter, based on the calculated mean free path or any other method seen fit. If the ray is determined to scatter by the DLL, it must then calculate the position along the ray path at which scatter occurred, the output ray direction cosines, and optionally the output electric field data for the ray.

The bulk scatter DLL will only be employed during ray tracing (analysis or layout) if the 'Scatter Rays' option is enabled. In that case, it will be called by OpticStudio once for each ray segment trace through a given volume. Note that if a ray bulk scatters, the output child segment is treated as a new segment, and can therefore undergo bulk scatter again (assuming the Object Properties are set to allow multiple scatter events, as is the default. The scattered ray output from the DLL will then again be passed into the DLL to determine whether scatter occurs a second time, and so on.

This DLL type is expected to have three functions: UserBulkDefinition, UserParamNames, and UserModelInformation. The bulk definition function is where we actually determine, based on the unscattered ray path and other data, whether the ray undergoes bulk scatter, and if so how the output ray is defined. As with many other non-sequential DLLs, the UserParamNames function is what defines the parameter names that are shown for user inputs in the UI. Finally, the UserModelInformation function simply determines whether the default bulk scatter models will be used or not (mean path and angle inputs). 

If the ray is to be scattered, the DLL should set data[10] equal to 1, and populate data values 35-37 with the output ray cosines, data 12-13 with the relative output energy and phase added, and set data 9 equal to the unscattered path distance. If the output electric field will be returned, data[10] should be set to 2, and the output E data should be populated in data entries 40-45. If the ray is determined not to scatter, the DLL should simply return 0 (keeping the original data[10] value equal to zero). A return value of -1 indicates an error. For a complete description of the data array passed between OpticStudio and the DLL, and for a demonstration in usage, please refer to the sample file ...\Documents\Zemax\DLL\BulkScatter\bulk_samp_1.c, or any of the examples in that directory.


Diffractive DLL (non-sequential)

The diffractive DLL is responsible for providing at a minimum, for a given incident ray and diffraction order, the phase added to the ray and the local phase slope at the ray intercept point. The output ray energy is also computed and returned by the DLL, meaning it is responsible for calculating the efficiency of the given diffraction order. Optionally, the full electric field data can be returned by the DLL.

Like the bulk scatter and other non-sequential DLLs, the diffractive DLL is likely called many times for each ray trace; the diffractive DLL will be applied to a given optical surface in the system, and each time a ray segment encounters that surface, the DLL will be called by OpticStudio. Furthermore, the diffractive DLL is only responsible for tracing a single diffraction order with each call. This means that if multiple diffraction orders are requested to be traced from a single incident ray, the diffraction DLL will be called multiple times for each incident ray segment. The same could be said if the ray splits at the diffractive surface, and diffracted rays are to be traced in both reflection and transmission. It is possible that the ray energy leaving the diffractive surface might be highly attenuated (i.e. for a high diffraction order, etc.). Even in this case, the DLL should return the ray data; OpticStudio is always responsible for internally deciding when a ray will be terminated due to energy thresholds. 

For each ray hitting the diffractive surface, OpticStudio calls the DLL 2*(Stop Order - Start Order + 1) times. This means that OpticStudio will call the DLL for each order in transmission and reflection. The DLL is using data[11] = 0 for transmission and data[11] = 1 for reflection. If for any reason the DLL returns a ray in the opposite direction to what OpticStudio requested, the DLL must subsequently change data[11]. For example, if OpticStudio calls the DLL with data[11] = 0, which means a request for data in transmission, if the DLL returns data in reflection, the DLL must change data[11] to 1 before returning to OpticStudio.

The DLL should return a value of either 0 or -1. 

  • If 0 is returned, it means there is no error in the calculation. 
  • If -1 is returned, if means that something is wrong. It can be non-physical user-input parameters or unusual incident ray data. When OpticStudio received a return of -1, a geometric error is issued and ray-tracing may stop depending on whether the user has turned on the "Ignore Error" feature. It's suggested to only return -1 when there is a critical error.
    If the ray that OpticStudio requests can simply not launch because it's not physical or because it results in a calculation error, the DLL can set both data[30] (relative energy) and data[31] (flag to indicate if phase and derivative are returned) to zero and return 0, instead of returning -1. 
    Please note that this is only a suggestion. Both options exist and it is up to the programmer to decide how the DLL will work.

Upon launching the DLL, OpticStudio will send the incident ray data (intercept and angle, wavelength, electric field data) as well as the surface normal at the intercept point, and information about the index and surface properties of the diffractive surface. The current order being traced, as well as the start and stop order are also passed to the DLL The DLL is responsible for populating data values 30 (specifying the relative energy out) and 31 (specifying what type of ray data will be returned). If data[31] equals 1, then the DLL must populate data 32-34, the phase added to the ray and phase slope in X and Y. If data[31] equals 2, it must also calculate data 35-45, the complete output ray data. The DLL should return 0 if successful, and -1 if it encountered an error. For a complete description of all the data items passed between ZOS and the Diffraction DLL, please see the example {Zemax}\DLL\Diffraction\diff_samp_1.c.

Note when the data[31] is 2, the power of diffracted ray is mainly calculated by the returned field data in data[40-45]. However, the data[30] can still affect the ray-tracing in two ways. One is that OpticStudio will use data[30] for estimating the Lost Energy, which shows in the Ray Trace dialog, during raytracing. One is that the diffractive ray will stop tracing if the value in data[30] is smaller than Minimum Relative Ray Intensity in the System Explorer.

* The maximum number of parameters we can define for the DLL is 40 for now. (2023 June)

* Another diffraction DLL interface for 2D diffraction grating has been release in 2022. It supports more function, such like dynamic parameter name and feedback suggested parameter value. See "Diff2DSample.cpp" in \Documents\Zemax\DLL\Diffractive\ for an example.


Gradient Index DLL (non-sequential)

When a ray is traced through a non-sequential gradient index material, OpticStudio treats it as a series of refractive events, with the step-size between refractive events determined by the user. A Gradient Index DLL is responsible for determining the local index of refraction and the local gradient of the index, at a given XYZ position within the medium. After a given propagation distance through the medium has been traversed (set by the OpticStudio user in the UI), OpticStudio again calls the gradient index DLL with the new ray coordinates. The refraction and subsequent re-propagation of the ray within the medium is handled by OpticStudio; once the re-propagated ray has traversed another step-length within the gradient index medium, OpticStudio will again automatically call the DLL to find the new index, and the process repeats until the ray exits the medium. As such, the Gradient Index DLL is quite simple, and is only responsible for reporting the value of the index function at the requested coordinates.

There are three functions in the Grin DLL which may be called by OpticStudio: UserGrinFunction, UserParamNames, and UserParamScale. The UserParamNames function is one we've seen before, and is simply used to populate the OpticStudio UI with the appropriate parameter names (visible in the menu Object Properties > Index > GRIN). Secondly, the UserParamScale function is called by OpticStudio when the user employs the native 'Scale Lens' tool within OpticStudio, and is included to allow assurance that if the lens data is scaled, length-unit parameter values are scaled correctly by the tool. Finally, the UserGrinFuntion is where we actually define the 3D gradient index function. This function should populate data values 6-9 with the index and index gradient data. All of these functions should return 0 if executed successfully, and -1 if an error occurred.

For a complete description of the data items and a demonstration of use, see the example ...\Documents\Zemax\DLL\GRIN\grin1.c, or any of the example files in that directory.



Source DLL (non-sequential Source Object)

The source DLL is called each time a ray trace is initialized (for either layout rays or analysis rays, including the NSTR operand, etc.) and the source object's corresponding #Rays parameter is non-zero. Like most other Non-Sequential DLLs, the source DLL which describes a Non-Sequential Source Object is called for just one ray at a time. For each ray (given by the “# Layout Rays” or “# Analysis Rays” parameters), it must provide a starting position (XYZ) and orientation (LMN) as well as a relative intensity value. The DLL passes this data back to OpticStudio by populating data values 1-7 in the data structure used in the UserSourceDefinition function call.

By probabilistically choosing the XYZ and LMN values, and keeping a relative intensity of 1 for all rays, the output intensity profile of the source will follow the given probability distribution in both the near- and far-field. Alternatively, the relative intensity of each ray could be scaled via a probability function, and the initial ray coordinates and direction cosines can be chosen more systematically (i.e. each initial ray positions/directions are uniformly distributed, but initial relative intensity values are scaled based on the desired source intensity distribution). By convention, native OpticStudio source objects use the former method for simulating non-uniform intensity distributions as opposed to the latter, but either is acceptable in your custom DLL. 

The Source DLL has two functions: UserParamNames and UserSourceDefinition. The first is called when the DLL is loaded into the Non-Sequential Component Editor, and is used to define the displayed column header names for the parameter columns. The UserSourceDefinition function is called during raytracing, to generate rays from the source definition. Both of these functions should return 0 if there was no error, and -1 if they encounter an error.

For a full listing of the data values passed between OpticStudio and the Source DLL, please refer to the sample file ...\Documents\Zemax\DLL\Sources\Cone.cpp, or any of the other samples in that directory.


Surface Scatter DLL (non-sequential)

The Surface Scatter DLL allows the user to create a custom scatter model, which can then be applied to any surface (face) in the system. Similar to the Diffractive DLL, the Surface Scatter DLL is called by OpticStudio each time a ray segment interacts with the surface on which the scatter model is defined.

For a given incident ray intercept, directions, electric field, and wavelength, as well as the surface normal at the intercept, the DLL is responsible for computing the output energy and direction. Note that OpticStudio doesn't pass the incident ray data to the DLL, but rather the specular output ray data; in the case that an incident ray undergoes both transmittive and reflective scatter (i.e. it splits), the DLL will be called for each output scattered ray individually. However, OpticStudio also passes the information as to which path the ray is taking (if the output ray is being scattered in transmission or reflection), meaning a unique scatter definition could be used in each case. The incident and substrate media indices of refraction are also provided to the DLL. Optionally, the Scatter DLL can be set to return the full polarization data, and to work with the native Importance Sampling algorithm. 

If the DLL runs successfully, the data items 4-6, 10, 12, optionally 18 (for importance sampling), and optionally 40-45 (output E data) will be populated by the DLL. The DLL should return 0 if successful, and -1 if an error occurred.




Was this article helpful?
4 out of 4 found this helpful



Article is closed for comments.