A simulation program for ice halo phenomena. It traces light rays interacting with ice crystals to reproduce various halo patterns. Fast and efficient, supporting natural color rendering and multiple scattering.
Inspired by HaloPoint 2.0 and HaloSim 3.0.
-
High speed. The simulation program is 50
100 times faster than HaloPoint. On general cases this program runs at a speed of 140k200k rays per second. On multi-scattering cases it runs as fast as 50k~80k rays per second. -
Natural and vivid color. Based on the Spectrum Renderer project, this simulation program can render very natural and vivid color.

-
Full multi-scattering support. This program is designed to handle multi-scattering cases. It allows you to simulate any multi-scattering scenario freely. The following multi-scattering display is generated in 14 minutes with totally 72 million starting rays traced.

-
Customized crystal model. (not yet implemented) Support for custom crystal models via .obj file is planned but not yet re-implemented after the v3 rewrite. Currently only built-in crystal types (
prismandpyramid) are available.
After cloning, you can run the build script to build and install it.
./build.sh -j releaseIf everything goes well, the executable will be installed into build/cmake_install. And
you can run it like:
./build/cmake_install/Lumice -f config_example.jsonIt will output some information, as well as several rendered picture files.
If you are interested in more details, just go ahead to following sections.
- CMake >= 3.14
- Ninja (recommended, used as default build generator; install via
brew install ninjaon macOS orapt install ninja-buildon Ubuntu) - C++17 compatible compiler (GCC, Clang, or MSVC)
All other dependencies are automatically downloaded and managed via CPM.cmake:
- nlohmann/json v3.10.5 — JSON parsing (header-only)
- spdlog v1.15.0 — Logging (header-only)
- tl-expected v1.1.0 —
expected<T,E>for C++17 (header-only) - GoogleTest v1.15.2 — Unit testing (downloaded when
-tis enabled)
Note on Ninja: If Ninja is not installed, you can remove
-G Ninjafrombuild.shto fall back to the system default generator (usually Unix Makefiles).
A build script is provided to simplify the process.
With -h you will see help message:
./build.sh -h
Usage:
./build.sh [-tbjksxh] <debug|release|minsizerel>
Executables will be installed at build/cmake_install
OPTIONS:
-t: Build test cases and run test on them.
-b: Build benchmarks (Google Benchmark).
-j: Build in parallel, i.e. use make -j
-k: Clean build artifacts (keep dependency cache).
-x: Clean everything including dependency cache.
-s: Build shared library (default: static).
-h: Show this message.Note that debug version executables will not be installed, so they will be found in build/cmake_build.
GoogleTest is used for unit tests.
If -t option is set, the test cases will be built and run.
Configuration file contains all settings for a simulation. It is written in JSON format, parsed with nlohmann/json.
An example configuration file is provided: config_example.json.
For the complete configuration reference, see Configuration Guide. Below is a brief overview of each section.
Here is an example for one element:
"id": 2,
"type": "sun",
"altitude": 20.0,
"azimuth": 0,
"diameter": 0.5,
"spectrum": [
{"wavelength": 420, "weight": 1.0},
{"wavelength": 460, "weight": 1.0},
{"wavelength": 500, "weight": 1.0},
{"wavelength": 540, "weight": 1.0},
{"wavelength": 580, "weight": 1.0},
{"wavelength": 620, "weight": 1.0}
]A light_source section describes properties of light source. It may contain multiple elements corresponding to multiple light sources. They are referenced by id.
ID should be a unique number greater than 0. It is not necessary to keep IDs increasing one by one.
Fields azimuth, altitude describe position of the sun. They are in degrees, and so is diameter.
spectrum describes the spectrum of the light source. It can be either a standard illuminant name (e.g. "D65") or an array of wavelength-weight objects. Wavelength determines refractive index, whose data is from
Refractive Index of Crystals.
Here is an example for one element:
"id": 3,
"type": "prism",
"shape": {
"height": 1.3,
"face_distance": [1, 1, 1, 1, 1, 1]
},
"axis": {
"zenith": {
"type": "gauss",
"mean": 90,
"std": 0.3
},
"roll": {
"type": "uniform",
"mean": 0,
"std": 360
},
"azimuth": {
"type": "uniform",
"mean": 0,
"std": 360
}
}A crystal section stores all crystals used in simulation. It may contain multiple elements (different crystals). They are referenced by id.
zenith, roll and azimuth (optional):
These fields define the pose of crystals. zenith defines the c-axis orientation, and roll
defines the rotation around c-axis.
They are of distribution type, which can be a scalar, indicating a deterministic distribution, or can be a tuple of (type, mean, std) describing a uniform or Gaussian distribution. All angles are in degrees.
type and shape: they describe the shape of a crystal.
Currently there are 2 kinds of crystals, prism and pyramid.
Each type has its own shape parameters.
-
prism(hexagonal prism): Parameterheightdefinesh / awherehis the prism height,ais the diameter along a-axis (also x-axis in the program). It is of distribution type. Default:1.0.face_distancedescribes an irregular hexagonal face (see below). Default:[1, 1, 1, 1, 1, 1].
. -
pyramid(hexagonal pyramid):{upper|lower|prism}_hdescribe heights of each segment, see picture below.{upper|lower}_hrepresenth1 / H1andh3 / H3respectively, whereH1means the max possible height for upper pyramid segment, andH3the same but for lower pyramid segment.prism_hhas the same meaning as forprism.
.
{upper|lower}_indicesare Miller index describing the pyramidal face orientation. Default:[1, 0, 1]. -
face_distance: The distance here means the ratio of actual face distance to a regular hexagon distance. A regular hexagon has distance of[1, 1, 1, 1, 1, 1]. The following figure shows an irregular hexagon with distance of[1.1, 0.9, 1.5, 0.9, 1.7, 1.2]
.
Here are two common examples:
[
{
"id": 3,
"type": "raypath",
"raypath": [3, 5],
"symmetry": "P"
},
{
"id": 4,
"type": "entry_exit",
"entry": 3,
"exit": 5,
"action": "filter_in"
}
]type: can be one of these types: raypath, entry_exit, direction, crystal, complex, none.
Here is an example:
"id": 3,
"light_source": 2,
"ray_num": 1000000,
"max_hits": 7,
"scattering": [
{
"crystal": [1, 2, 3],
"prob": 0.2
},
{
"crystal": [2, 3],
"proportion": [20, 100],
"filter": [2, 1]
}
]Here is an example:
"id": 3,
"lens": {
"type": "linear",
"fov": 40
},
"resolution": [1920, 1080],
"view": {
"azimuth": -50,
"elevation": 30,
"roll": 0,
"distance": 8
},
"visible": "upper",
"background": [0, 0, 0],
"ray": [1, 1, 1],
"opacity": 0.8,
"grid": {
"central": [
{
"value": 22,
"color": [1, 1, 1],
"opacity": 0.4,
"width": 1.2
}
],
"elevation": [],
"outline": true
}view: describes camera pose.
lens: lens type, can be one of these values: linear, fisheye_equal_area, fisheye_equidistant, fisheye_stereographic, dual_fisheye_equal_area, dual_fisheye_equidistant, dual_fisheye_stereographic, rectangular.
You can use fov (field of view in degrees) or f (focal length in mm) to specify the lens. If f is used, the program automatically calculates the corresponding fov.
Nothing complicated. It just keeps references to scene and render.
For detailed documentation, please refer to:
- Documentation Index - Navigation and index of all documents
- Configuration Guide - Complete configuration reference
- Architecture Document - System architecture
- Developer Guide - Developer guide
- C API Documentation - C interface usage
- API Documentation - Auto-generated API docs (generate locally with
doxygen .doxygen-config)