Generating abstract wallpapers using 3D shaders​
​by Silvia Hao
As you can see from the cover GiF, unlike my previous posts, this is a piece of light reading for some quick fun. It came to my mind when I was exploring the shader features for 3D graphics. The idea is very simple: given an color image, I want to generate a 3D mesh that resembles the original image, where the hue is used as base color of the mesh, but the brightness should come from lighting and shading. Hopefully the end result will make you smile :)

Helper functions

ClearAll[pipe,pipeList,branch,branchSeq]​​pipe=RightComposition;​​branch=Through@*{##}&;​​branchSeq=pipe[branch@##,Apply@Sequence]&;​​pipeList=pipe[Fold[branchSeq[Identity,pipe[#2,#1]]&,Identity,Reverse@{##}],List]&;

From gray scale to shading density

Since version 12.1, various shaders were added to Wolfram Language - StippleShading, HalftoneShading, HatchShading, etc. Last year we got a physically based MaterialShading.
Take StippleShading for example, it applies on 3D surfaces, maps lighting luminosity to dots density:
In[]:=
Graphics3D[{StippleShading[],Cylinder[{{1,0,0},{1,0,-1}},.5],Cone[]},LightingDirectionalLight[White,{1,1,1}]]
Out[]=
The documentation mentioned:
The density of dots is driven by the luminosity of the surface. Less dense areas indicate high luminosity.
..., but didn’t say how exactly. But of course that didn’t stop me trying. Here a white DirectionalLight is set along
Z
axis, assuming Lambert's Law when the light is diffused by an inclined InfinitePlane, then the luminosity of the plane is proportional with
cos(α)
.
In[]:=
Manipulate​​Grid[{{​​Graphics3D[{​​EdgeForm[],InfinitePlane[{0,0,0},{{1,0,0},{0,Cos[α],Sin[α]}}]​​}​​,Lighting{DirectionalLight[White,{0,0,1}]}​​,PlotRange{{-1,1},{-Cot[α],Cot[α]},{-1,1}},PlotRangePaddingNone​​,ViewPointFront,ViewVertical{0,0,1},ViewProjection"Orthographic",ViewAngle1​​,BackgroundBlack,BoxedFalse,ImageSize100×.6]​​,Dynamic[Image[{{GrayLevel[Cos[α]//N]}},ImageSize100×.6]]​​}}]​​,α,
π
4
,0,
π
2
​​,ControlPlacementTop,SaveDefinitionsTrue
Out[]=
​
α
For any given α, rasterizing the graphics and computing the average gray scale further confirmed the Lambert's Law is in use:
Function[α​​,Graphics3D[{​​EdgeForm[],InfinitePlane[{0,0,0},{{1,0,0},{0,Cos[α],Sin[α]}}]​​}​​,Lighting{DirectionalLight[White,{0,0,1}]}​​,PlotRange{{-1,1},{-Cot[α],Cot[α]},{-1,1}},PlotRangePaddingNone​​,ViewPointFront,ViewVertical{0,0,1},ViewProjection"Orthographic",ViewAngle1​​,BackgroundBlack,BoxedFalse,ImageSize100×.6]//pipe[​​Rasterize,ColorConvert[#,"Grayscale"]&​​,ImageData[#,"Real"]&,Flatten,Mean​​]​​]/@Subdivide0,
π
2
,10[[2;;-2]]//Iconize
Out[]=
Subdivide0,
π
2
,10[[2;;-2]]//Cos,
//​​ListLinePlot[#,MeshFull,FrameTrue,FrameTicksAll,FrameLabel{"cos(α)","gray scale"}]&
Out[]=
Now let me check the StippleShading’s effect:
In[]:=
Manipulate​​Graphics3D[{​​StippleShading[d],EdgeForm[],​​InfinitePlane[{0,0,0},{{1,0,0},{0,Cos[α],Sin[α]}}]​​}​​,Lighting{DirectionalLight[White,{0,0,1}]}​​,PlotRange{{-1,1},{-Cot[α],Cot[α]},{-1,1}},PlotRangePaddingNone​​,ViewPointFront,ViewVertical{0,0,1},ViewProjection"Orthographic",ViewAngle2​​,BackgroundBlack,BoxedFalse,ImageSize300×.6]​​,α,
π
4
,0,
π
2
​​,{{d,.5},0,1}​​,ControlPlacementTop,SaveDefinitionsTrue
Out[]=
​
α
d
For
d=0.5
(i.e. default StippleShading[]), a similar test can be done:
In[]:=
Function[α​​,Graphics3D[{​​StippleShading[],EdgeForm[],​​InfinitePlane[{0,0,0},{{1,0,0},{0,Cos[α],Sin[α]}}]​​}​​,Lighting{DirectionalLight[White,{0,0,1}]}​​,PlotRange{{-1,1},{-Cot[α],Cot[α]},{-1,1}},PlotRangePaddingNone​​,ViewPointFront,ViewVertical{0,0,1},ViewProjection"Orthographic",ViewAngle2​​,BackgroundBlack,BoxedFalse,ImageSize300×.6]//pipe[​​Rasterize,ColorConvert[#,"Grayscale"]&​​,ImageData[#,"Real"]&,Flatten,Mean​​]​​]/@Subdivide0,
π
2
,100[[2;;-2]]//Iconize
Out[]=
In[]:=
​​(*f(α):*)Subdivide0,
π
2
,100[[2;;-2]]//Cos//#(2-#)&​​,(*g:*)
​​//ListPlot[#,FrameTrue,FrameTicksAll,FrameLabel{"f​(α)","Grayscale"}]&
Out[]=

Separating gray scale for 3D shading

Take the following image for example:
Credit: Yellow flower field under blue sky during daytime by Brandon LeClaire, from Unsplash.
Basically imgPremix is the projection of img onto RGB gamut’s boundary:

Generating abstract arts with shaders

A popular choice for abstract painting is triangular mesh. Here I create one from a rectangle the same size as the original image, so later on I can query pixel values with ease:
For example, ImageValue[img,#]& can be applied on vertices’ coordinates directly:
However, as said before, instead of img, I’ll use colors from imgPremix, and tilt each triangular cells according to fraction, so they all get proper shades from a heads-down light. So instead of query colors for each vertices through MeshCoordinates[canvas], I should query for each triangles, say, through AnnotationValue[{canvas,2},MeshCellCentroid]:
Put every pieces together, here is the self-contained code:
Different variations are easy to create. For example I can boost colors with ImageAdjust, modify grayscale-to-angle transformation to ArcCos[g], use a different shader, etc.
Or even start from a non-triangular mesh:
Finally I should mention: to keep the post simple, I didn’t consider gamma correction for original color image. Readers interested in it can refer to my previous Community post Color notes: alpha-blending done properly. An super exciting shader I didn’t cover in this post is MaterialShading. I surely hope to explore it in another day!