Message Boards Message Boards

Photorealistic Rendering: A walkthrough for working with LuxCoreRender

Cover Image

Couple of days ago, in Wolfram's Wechat group there was a pleasant discussion about Tim's neat post Ray-tracing Graphics3D with UnityLink. I then promised a walkthrough on how to work with LuxCoreRender. I think it would be nice to share my experience not only in the Wechat group but also here to the Community.

LuxCoreRender is a cross-platform render engine published under GPLv3 license. It provides nice features and both Python and C++ APIs. According to the official site, it's a physically correct, unbiased rendering engine. Usually LuxCoreRender can be accessed as a plugin in many popular 3D tools such as Blender, but in this post I'll talk about the way more suitable for scripting from Mathematica's side.

First of all, let's set our working directory.

SetDirectory[NotebookDirectory[]];

Import and Export LuxCoreRender SDL files

Readers can safely skip this section. We'll just dump our import and export functions for the LuxCoreRender SDL files here. Please use them with caution. Though I have tested them with some examples, edge cases can bite.

ClearAll[branch]
branch = Through@*{##} & ;
ClearAll[luxcoreReaderInternalStep, luxcoreReader]

luxcoreReaderInternalStep[e : {__List}] := e // RightComposition[
     Map[If[Length[#] == 1, #[[1]], #] &],
     GroupBy[ListQ],
     If[KeyExistsQ[#, True], # // MapAt[GroupBy[First -> Rest] /* Map[luxcoreReaderInternalStep], Key[True]], #] &,
     If[KeyExistsQ[#, False], # // MapAt[Apply[Sequence], Key[False]], #] &,
     Values,
     Switch[#,
            {_Association}, #[[1]],
            _, #
            ] &
     ]

luxcoreReader =
 Module[{numReader = ImportString[#, "Table"][[1, 1]] &},
        RightComposition[
              StringReplace[(StartOfLine ~~ {Whitespace, "#" ~~ Except["\n"] ...} ... ~~ "\n") .. :> ""],
              StringTrim,
              StringSplitNested[#, {"\n", Whitespace ~~ "=" ~~ Whitespace}] &,
              MapAt[StringTrim, {;; , ;;}],
              MapAt[StringSplit[#, "."] &, {;; , 1}],
              Map[Flatten],
              luxcoreReaderInternalStep,
              Map[Last, #, {-2}] &,
              Map[
                  Switch[#,
                         _?(StringMatchQ["\"" ~~ ___ ~~ "\""])                    , StringTake[#, {2, -2}],
                         _?(Not@*StringFreeQ[Whitespace])                         , StringSplit[#, Whitespace] // Map[#0],
                         _?(StringMatchQ[{DigitCharacter, ".", "+", "-", "e"} ..]), numReader@#,
                         _, #
                         ] &
                  , #, {-1}] &
              ]
       ];
ClearAll[luxcoreWriter]

luxcoreWriter =
 Module[{
         keychainCombiner = Cases[Key[key_] :> key] /* (Riffle[#, "."] &),
         stringifier = ExportString[#, "RawJSON", "Compact" -> 0] &
        },
        RightComposition[
               MapIndexed[Sow[{##}] &, #, {-1}] &, Reap, #[[2, 1]] &
               , {#, Range[Length@#]}\[Transpose] &
               , GroupBy[IntegerQ[#[[1, 2, -1]]] &]
               ,(* atomic values: *)
               MapAt[
                     MapAt[Apply[{#1 // stringifier, #2 // keychainCombiner} & /* Apply[StringJoin[#2, " = ", #1] &]], {;; , 1}]
                     , Key[False]]
               ,(* numerical arrays: *)
               If[# // KeyExistsQ[True]
                  , # // MapAt[
                    RightComposition[
                           MapAt[Apply[{#2 // Most, {Last[#2], #1}} &], {;; , 1}]
                           , GroupBy[(#[[1, 1]] &) -> ({#[[1, 2]], #[[2]]} &)]
                           , Map@branch[
                                  (#[[;; , 1, 2]] &) /* stringifier /* (StringReplace[{"," -> " ", "[" | "]" :> ""}])
                                  , #[[1, 2]] &
                                  ]
                           , KeyMap[keychainCombiner]
                           , KeyValueMap[{StringJoin[#1, " = ", #2[[1]]], #2[[2]]} &]
                           ]
                    , Key[True]]
                  , #] &
               , Values, Apply@Join, SortBy[Last], #[[;; , 1]] &
               , Riffle[#, "\n"] &, StringJoin
               ]
        ];

Anchor the world

Before any rendering happens, a scene is necessary. The center of our scene is going to be the origin of our "world" coordinate system.

stageCenter = {0, 0, 0};

Lighting

Spotlight

On a stage, the most indispensable thing is lighting. Among all the lights, nothing attracts more attention than a spotlight. To project a spotlight onto the stage, we need to determine its position, aiming target, color, size, etc. (Reference: "Spot" in documentation of Lighting )

spotLightPos = {4.5, 0, 6};

Clear[spotLight]
spotLight := <|"type" -> "spot"
               , "position" -> spotLightPos, "target" -> stageCenter
               , "color" -> RGBColor[1, 1, 1], "power" -> 100, "efficency" -> 17, 
               "gain" -> {1, 1, 1}
               , "coneangle" -> 14 \[Degree] (* <- half angle of the cone *)
               , "conedeltaangle" -> 1 \[Degree]
               |> //
      Function[cfg
               , cfg // 
                RightComposition @@ 
                 MapThread[
                     If[KeyExistsQ[cfg, #2], MapAt[##], Identity] &
                        , {
                             {"color"         , List @@ ColorConvert[#, "RGB"] &}
                           , {"transformation", TransformationMatrix /* Transpose /* Flatten}
                           , {"coneangle"     , N[#]/Degree &}
                           , {"conedeltaangle", N[#]/Degree &}
                          }\[Transpose] // Reverse]
               ]

For more detail about lighting in LuxCoreRender, please refer to the official wiki and document page.

Camera

Rendering is like taking a photo, you carefully set up your camera, adjust the aperture and focal distance, and look for the the best angle of view.

We set up a camera aiming at the stage from an angle somewhat perpendicular to the light and focusing on the center. With this setting, we hope to capture a rich shades of light and shadow.

Perspective Camera

cameraPos        = {7., -7., 5.};
cameraTarget     = stageCenter + {-.2, 0, -.3};
cameraAimingAxis = cameraTarget - cameraPos;
cameraFocus      = stageCenter;

cameraUpVecF[ $\theta$,cameraAimingAxis] will be used to adjust the orientation of our film. Setting $\theta$ to $0^\circ$ and $90^\circ$ correspond to landscape and portrait.

Clear[cameraUpVecF]
cameraUpVecF = Function[{\[Theta], aimingAxis}, aimingAxis //
    RightComposition[
          RotationTransform[90 \[Degree], {aimingAxis, {0, 0, 1}}]
          , RotationTransform[\[Theta], aimingAxis]
          , Developer`FromPackedArray (* <- Currently our luxcoreWriter cannot handle PackedArray correctly *)
          ]];
Clear[camera]
camera := <|
     "type"          -> "perspective"
     (* (* use the default clipping *)
        ,"cliphither" -> 0           (* <- Working like minimum distance in M's ViewRange *)
        ,"clipyon"    -> \[Infinity] (* <- Working like maximum distance in M's ViewRange *)
     *)
   , "lookat"        -> <|"orig" -> cameraPos, "target" -> cameraTarget|>
   , "up"            -> cameraUpVecF[0 \[Degree], cameraAimingAxis]
   , "fieldofview"   -> 25 \[Degree]  (* <- Working like M's ViewAngle *)
   , "lensradius"    -> 0.1           (* <- Set a positive value to enable blurring by depth-of-field, 0 to disable it. *)
   , "focaldistance" -> Norm[cameraFocus - cameraPos]
   |> // Function[cfg
   , cfg // RightComposition @@
     MapThread[If[KeyExistsQ[cfg, #2], MapAt[##], Identity] &, {
         (* clip-range must be positive real number in luxcore renderer: *)
           {"cliphither"  , Clip[#, {.001, 1. 10^30}] &}
         , {"clipyon"     , Clip[#, {.001, 1. 10^30}] &}
         , {"fieldofview" , N[#]/Degree &}
         }\[Transpose] // Reverse]
   ]

For more detail about camera, please refer to the official wiki and document page.

Object

In LuxCoreRender, solid objects can be represented by meshes and materials. The easiest way to introduce mesh is through the PLY format. It's well-supported both in LuxCoreRender and in Mathematica.

Stage plane

We have been talking a lot about stage, but we haven't really got a stage yet. We are goint to forge a minimalist one with a square plane.

stage     = Polygon[Append[0] /@ CirclePoints[{5, \[Pi]/4}, 4]] // TranslationTransform[{0, 0, -1.7}];
stageMesh = stage // DiscretizeRegion // Export["stage_plane.ply", #, "PLY", "BinaryFormat" -> True] &;

We use LuxCoreRender's built-in Lambertian material to style our stage, with a neural gray diffuse color.

stageMaterial = <|"type" -> "matte", "kd" -> {0.5, 0.5, 0.5}|>;

The full configuration is

stageConfig = <|
     "objects"   -> <|"stage_plane" -> <|"ply" -> stageMesh, "material" -> "stage_mat"|>|>
   , "materials" -> <|"stage_mat"   -> stageMaterial                                    |>
   |>;

Main Role

Now everything on the stage is ready except for the star. We are going to put a diamond at the center.

TruncatedPolyhedron is a handy function to cut a gem. With some additional positioning, in two lines we have our diamond mesh ready.

gem = Fold[TruncatedPolyhedron, Octahedron[], ConstantArray[.3, 2]] //
             ScalingTransform[1.3 {1, 1, 1}] //
             RotationTransform[-10 Degree, {0, 0, 1}];
gem // Graphics3D[#, Boxed -> False] &
gemMesh = gem // DiscretizeGraphics // RegionBoundary // Export["gem.ply", #, "PLY", "BinaryFormat" -> True] &;

gem mesh as Graphics3D

Recall our camera is focusing on stage center, now we would like to adjust it to focus on the top facet of the diamond.

gemBd   = gem   // BoundaryDiscretizeGraphics;
facetID = gemBd // {MeshCellIndex[#, 2], PropertyValue[{#, 2}, MeshCellCentroid]}\[Transpose] & // 
                   MaximalBy[#[[2, -1]] &] // #[[1, 1]] &;

Or, more specifically, on one of the facet's corners nearest to our camera. Because we used delayed definition (reference: SetDelayed), our camera will be automatically up-to-date everytime we query it.

cameraFocus = MeshPrimitives[gemBd, facetID][[1]] // MinimalBy[Norm[# - cameraPos] &] // First;
Graphics3D[{gem, {Red, Sphere[cameraFocus, .05]}}
           , ViewVector -> Values[camera["lookat"]]
           , Boxed -> False]

gem mesh, highlight the focus-on point

To emulate a real diamond, we need LuxCoreRender's built-in "glass" material with refractive index of diamond.

gemIOR = Entity["Mineral", "Diamond"]@EntityProperty["Mineral", "RefractiveIndices"] // First
(* Out[]= 2.418 *)

For the dispersion, LuxCoreRender uses Cauchy's empirical formula. We'll set it to a very high value. (For details about the formula, please refer to http://scienceworld.wolfram.com/physics/CauchysFormula.html .)

gemDispersion = 0.02;

gemMaterial = <|
     "type"        -> "glass"
   , "kr"          -> {1, 1, 1}     (* <- reflected color *)
   , "kt"          -> {1, 1, 1}     (* <- transmited color *)
   , "interiorior" -> gemIOR        (* <- refractive index *)
   , "cauchyc"     -> gemDispersion (* <- coefficient of \[Lambda]^-2 term in Cauchy's dispersion formula *)
   |>;

The full configuration is

gemConfig = <|
     "objects"   -> <|"gem"     -> <|"ply" -> gemMesh, "material" -> "gem_mat"|>|>
   , "materials" -> <|"gem_mat" -> gemMaterial                                  |>
   |>;

The Whole Scene

Combining all the lights, camera and objects together, we have a full specification of our scene.

sceneConfig = <|"scene" -> Join[
     <|
        "camera" -> camera
      , "lights" -> <|"spot_light_1" -> spotLight|>
      |>
     , Merge[{stageConfig, gemConfig}, Apply@Join]
     ]|>;

We use our exporter luxcoreWriter to format the scene configuration to match LuxCoreRender's scn DSL specification.

sceneFile = sceneConfig // luxcoreWriter // Export["diamond.scn", #, "String"] &
(* Out[]= diamond.scn *)

Render

Our post is already lengthy so I'm not going into details about the render configuration here. Just to point out that our importer/exporter can handle both scene describing file (.scn) and render configuration file (.cfg).

Another thing worth reminding readers not familiar with rendering is the choice of render strategy. As in this case we would like to see nice caustic from the diamond, "BIDIRCPU" engine coupled with "METROPOLIS" sampling is our best choice. We may talk about other methods in the future.

renderConfig = <|
   "scene"          -> <|"file"     -> sceneFile   |>
   , "renderengine" -> <|"type"     -> "BIDIRCPU"  |>
   , "sampler"      -> <|"type"     -> "METROPOLIS"|>
   , "light"        -> <|"maxdepth" -> 20          |>
   , "path"         -> <|"maxdepth" -> 20          |>
   , "batch"        -> <|"haltspp"  -> 2000        |>
   , "film" -> <|
                  "width" -> 1920, "height" -> 1080
                , "filter" -> <|"type" -> "NONE"|>
                , "imagepipelines" -> <| "0" -> <| "0" -> <|"type" -> "TONEMAP_AUTOLINEAR"               |>
                                                 , "1" -> <|"type" -> "GAMMA_CORRECTION", "value" -> 2.5`|>
                                                 |>
                                       , "1" -> <| "0" -> <|"type" -> "INTEL_OIDN"                       |>
                                                 , "1" -> <|"type" -> "TONEMAP_AUTOLINEAR"               |>
                                                 , "2" -> <|"type" -> "GAMMA_CORRECTION", "value" -> 2.5`|>
                                                 |>
                                       |>
                , "outputs" -> <| "0" -> <|"index" -> 0, "type" -> "RGB_IMAGEPIPELINE", "filename" -> "diamond_original.png"|>
                                , "1" -> <|"index" -> 1, "type" -> "RGB_IMAGEPIPELINE", "filename" -> "diamond_denoised.png"|>
                                |>
                |>
   |>;

renderCfgFile = renderConfig // luxcoreWriter // Export["diamond.cfg", #, "String"] &

We can import the configuration file and browse it with Dataset as easy.

Import[renderCfgFile, "String"] // StringTrim // luxcoreReader // Dataset

rendering configuration

To start rendering, we use the portable LuxCoreRender Standalone release v2.2 from https://luxcorerender.org/download/ . (The one I used is Python 3.7 and Windows 64bit with OpenCL support).

After extracting you'll find two executables pyluxcoretool and luxcoreui. You can either start the renderer from console by executing (from our working directory, which is Directory[])

pyluxcoretool console diamond.cfg

or by loading the .cfg file through Rendering » Load menu in luxcoreui (which has a simple GUI).

To be lazy and stay in Mathematica, on Windows you can invoke the console as following. (Please remember to change the luxbin to your actual path.)

luxbin = "path\\to\\your\\pyluxcoretool.exe";

luxPS = StartProcess[{$SystemShell, "/C",
   StringTemplate[
     "start /I /WAIT cmd /C \"\"`bin`\" console \"`cfg`\"\""][<|
     "bin" -> luxbin,
     "cfg" -> renderCfgFile
     |>]
   }]

When the render process finishes, we shall find the results under our current working directory with filenames according to renderConfig[["film","outputs",;;,"filename"]].

We have set the render halt condition to 2000 samples (renderConfig[["batch","haltspp"]]). For this scene it will take almost 1 hour on a computer with Intel Core i7-8750H CPU. ("BIDIRCPU" engine does most job on CPU.) For quick preview rendering, you can set it to smaller values (e.g. 20). Smaller renderConfig[["film",{"width","height"}]] will also reduce the computing time (with smaller resulting images). Additionally, you can always break the render process anytime (on Windows press Ctrl + C in cmd, or click the Rendering » Pause menu in luxcoreui). The intermediate result will be saved to disk for the console mode, or can be manually saved from Film » Save outputs menu in luxcoreui.

Epilog

Hopefully you have got a beautiful photo of the diamond like our cover image.

So far we have demonstrated in Mathematica how to setup a simple scene and render it with LuxCoreRender. We have plans to talk about more advanced topics (like HDRI, volume, how to generate smooth mesh, etc.) in the fufuture.

References

Bounus

  • Click the cover image to get the full sized version!

  • Find the Notebook for this post below!

Attachments:
POSTED BY: Silvia Hao
10 Replies

Beautiful render Sylvia! I feel like an amateur now, which I am. I can envision some true Mathematica(l) art being generated with this workflow. Thank you for the detailed description and notebook!

POSTED BY: Tim Laska

Thanks for the nice word Tim, and thank you very much for your inspiring post!

I think, comparing to other interface of LuxCoreRender (like Blender), Mathematica makes a unique and outstanding difference by how easily one can interactive with everything in the Notebook or shape a script afterwards. LuxCoreRender's SDL maps to Mathematica's Association and Dataset perfectly. And it can be more intuitive and easier to do with Mathematica code than by dragging sliders on GUI when one wants to manipulate a mesh precisely. Not to mention all the built-in advanced geometry and region functions and image processing functions. I'm actually tempted to extend this into a one-stop complete solution.

POSTED BY: Silvia Hao

Very cool! Thanks very much for sharing!!

POSTED BY: Sander Huisman

Incredible! While I've used Mathematica a bit (at university) and LuxCoreRender since it was LuxRender, mathematically generating scenes with the flexibility this gives reminds me of Pov-Ray, except both the engine and scene generation are more powerful.

A tip for speed, using the Hybrid Backwards/Forwards path tracer will produce much faster results than Bi-Dir, while getting the same kind of results: https://forums.luxcorerender.org/viewtopic.php?f=5&t=1187

Thanks a lot for your tip Alan, I didn't aware of that! I did some test according to the forum post you kindly pointed out. The hybrid tracer does make quite an improvement on performance when working with PATHOCL. I must confess I'm still a newcomer and have lots to learn.

POSTED BY: Silvia Hao

Thanks for posting this amazing walkthrough! I'm new to luxcorerender and am trying to get this notebook to work. When I evaluate the notebook and run pyluxcoretool, it emits this relatively unhelpful error message:

C:\Users\Ben\Downloads\luxcorerender-v2.2-blender2.80-win64-opencl\luxcorerender-v2.2-blender2.80-win64-opencl
? pyluxcoretool console diamond.cfg
[MainThread][2019-12-01 18:41:00,669] LuxCore 2.2
Traceback (most recent call last):
  File "pyluxcoretool.py", line 59, in <module>
  File "C:\Users\Ben\AppData\Local\Temp\_MEI112042\pyluxcoretools.zip\pyluxcoretools\pyluxcoreconsole\cmd.py", line 174, in main
  File "C:\Users\Ben\AppData\Local\Temp\_MEI112042\pyluxcoretools.zip\pyluxcoretools\pyluxcoreconsole\cmd.py", line 126, in LuxCoreConsole
RuntimeError: Syntax error in a Properties at line 1
[20124] Failed to execute script pyluxcoretool

I noticed when I evaluated the notebook that some warnings were thrown when generating diamond.scn, which I believe to be the problem:

In[35]:= sceneFile = 
 sceneConfig // luxcoreWriter // Export["diamond.scn", #, "String"] &

During evaluation of In[35]:= StringJoin::string: String expected at position 2 in scene.camera.lookat.orig = <>1<>7.<>2<>2<>(-7.)<>3<>3<>5.<>4.

During evaluation of In[35]:= StringJoin::string: String expected at position 3 in scene.camera.lookat.orig = <>1<>7.<>2<>2<>(-7.)<>3<>3<>5.<>4.

During evaluation of In[35]:= StringJoin::string: String expected at position 4 in scene.camera.lookat.orig = <>1<>7.<>2<>2<>(-7.)<>3<>3<>5.<>4.

During evaluation of In[35]:= General::stop: Further output of StringJoin::string will be suppressed during this calculation.

During evaluation of In[35]:= Part::partw: Part 2 of branch[#1[[1;;All,1,2]]&/*(ExportString[#1,RawJSON,Compact->0]&)/*StringReplace[{,-> ,[|]:>}],#1[[1,2]]&][{{{1,7.},2},{{2,-7.},3},{{3,5.},4}}] does not exist.

During evaluation of In[35]:= Part::partw: Part 2 of branch[#1[[1;;All,1,2]]&/*(ExportString[#1,RawJSON,Compact->0]&)/*StringReplace[{,-> ,[|]:>}],#1[[1,2]]&][{{{1,-0.2},5},{{2,0},6},{{3,-0.3},7}}] does not exist.

During evaluation of In[35]:= Part::partw: Part 2 of branch[#1[[1;;All,1,2]]&/*(ExportString[#1,RawJSON,Compact->0]&)/*StringReplace[{,-> ,[|]:>}],#1[[1,2]]&][{{{1,-3.80007},8},{{2,3.69452},9},{{3,10.0419},10}}] does not exist.

During evaluation of In[35]:= General::stop: Further output of Part::partw will be suppressed during this calculation.

Out[35]= "diamond.scn"

I'm not sure why this is throwing this error -- do you know what the issue is? This is running on Mathematica 12.0 and luxcorerender v2.2.

Thanks in advance!

POSTED BY: Ben Bartlett

On a lower-brow note, Ms. Hao has excellent coding style, viz. her use of whitespace and leading commas (indicating call sequence).

POSTED BY: Vincent Virgilio

Hi Ben,

I'm sorry I forgot to include the definition of a helper function branch in the Notebook.

It should go before the definition of luxcoreWriter. Please refer to the beginning of the Import and Export LuxCoreRender SDL files section of the original post above. Also I have updated the attached notebook in the original post.

Thank you for spotting the issue!

POSTED BY: Silvia Hao

Well I didn't invent that. I adopted it from gurus. I guess the take-away is WL does tolerant all kinds of coding styles one can imagine :)

POSTED BY: Silvia Hao

enter image description here - Congratulations! This post is now featured in our Staff Pick column as distinguished by a badge on your profile of a Featured Contributor! Thank you, keep it coming!

POSTED BY: EDITORIAL BOARD
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract