# How to Lego-fy your plots and 3D models...

Posted 6 years ago
28057 Views
|
25 Replies
|
69 Total Likes
|
 The other day I was thinking, could I make a certain plot in Lego? Well, Let's start at the start and make a simple n*m lego-brick. The basic measurements are: brickstyle=Sequence[Red,EdgeForm[AbsoluteThickness[1]]]; gropts=Sequence[Boxed->False,ViewVector->(10{2.4, -1.3, 2.}),ViewAngle->8*Degree]; dims={dimx,dimy,dimz}={8.0,8.0,9.6}/8; (* size of a unit cell in lego-world *) knobd=4.8/8; (* knob diameter *) knobh=1.8/8; (* knob height *) botdi=4.8/8; (* bottom pillar inner diameter*) botdo=6.51/8; (* bottom pillar outer diameter*) wall=1.2/8; (* wall thickness*) thickness=1.0/8; (* top thickness *) And here a function that will make simple brick: ClearAll[DrawLego] DrawLego[{nx_Integer,ny_Integer,nz_Integer:1},detailed:(True|False|None):True]:=Module[{ptsout,ptsin,sides,bottom,rimi,rimo,knobs,knobs2}, ptsout=Tuples[{{0,0,0},{nx,ny,nz}dims}\[Transpose]]; ptsin=Tuples[{{wall,wall,0},{nx,ny,nz}dims-{wall,wall,thickness}}\[Transpose]]; sides=If[BooleanQ[detailed],If[TrueQ[detailed],{ptsin,ptsout},{ptsout}],{ptsout}]; sides=GraphicsComplex[#,{Polygon[{1,2,6,5}],Polygon[{3,4,8,7}],Polygon[{1,2,4,3}],Polygon[{5,6,8,7}],Polygon[{2,4,8,6}]}]&/@sides; If[TrueQ[detailed], ptsout=Tuples[{{0,0,0},{nx,ny,0}dims}\[Transpose]]; ptsin=Tuples[{{wall,wall,0},{nx,ny,nz}dims-{wall,wall,thickness}}\[Transpose]]; rimo=ptsout[[1;;;;2]][[{1,3,4,2}]]; rimi=ptsin[[1;;;;2]][[{1,3,4,2}]]; rimo=Partition[rimo,2,1,1]; rimi=Partition[rimi,2,1,1]; bottom=MapThread[Polygon[#1~Join~Reverse[#2]]&,{rimo,rimi}]; ]; If[BooleanQ[detailed], knobs=Tuples[Range[1,#]&/@({nx,ny})]-1/2; knobs=Cylinder[{Append[#{dimx,dimy},nz dimz],Append[#{dimx,dimy},nz dimz+knobh]}&/@knobs,knobd/2]; ]; If[TrueQ[detailed], knobs2=Tuples[Range[1,#]&/@({nx,ny}-1)]; knobs2=Tube[{Append[#{dimx,dimy},0],Append[#{dimx,dimy},nz dimz-thickness]}&/@knobs2,botdo/2]; ]; If[BooleanQ[detailed], If[TrueQ[detailed], {sides,bottom,knobs,{CapForm[None],knobs2}} , {sides,knobs} ] , {sides} ] ] DrawLego[{nx_Integer,ny_Integer,nz_Integer:1},p:{px_,py_,pz_},detailed_:True]:=Translate[DrawLego[{nx,ny,nz},detailed],p{1,1,dimz}-{0.5,0.5,0}] So we can draw any brick now, at any place, and we have the option to have it detailed or not... Graphics3D[{brickstyle, DrawLego[{4, 2}]}, Lighting -> "Neutral", Boxed -> False, Axes -> True] Graphics3D[{brickstyle, DrawLego[{4, 2, 1}]}, Lighting -> "Neutral", Boxed -> False, Axes -> True] Graphics3D[{brickstyle, DrawLego[{4, 2, 1}, {1, 1, 1}]}, Lighting -> "Neutral", Boxed -> False, Axes -> True] Graphics3D[{brickstyle, DrawLego[{4, 2, 1}, {1, 1, 1}, False]}, Lighting -> "Neutral", Boxed -> False, Axes -> True] Graphics3D[{brickstyle, DrawLego[{4, 2, 1}, {1, 1, 1}, None]}, Lighting -> "Neutral", Boxed -> False, Axes -> True] giving:So now that we can 'plot' any brick we can make a function that will cover a layer in brick-world with bricks of decreasingly smaller sizes iteratively, and will alternately go in the horizontal x and y directions: ClearAll[TileWithLego,CreateLegos,TransformLego] TileWithLego[slice_List/;MatrixQ[slice],sizes_List,greedy:(True|False),bricks_List:{}]:=Module[{size,bounds,sizex,sizey,shift,dims,dimx,dimy,greedy\[Lambda],stepi,stepj,newarr,part,newbricks}, size={sizex,sizey}=First[sizes]; dims={dimy,dimx}=Dimensions[newarr=slice]; shift=Floor[First[size]/2]; {stepi,stepj}=If[greedy,{1,1},{sizex,sizey}]; greedy\[Lambda]=Boole[!greedy]; newbricks=Reap[Do[ bounds={{j,j+sizey-1},{i,i+sizex-1}}; part=Take[newarr,##]&@@bounds; If[Total[part,2]===sizex sizey, newarr[[Span@@bounds[[1]],Span@@bounds[[2]]]]=0; Sow[bounds]; ] , {j,1,dimy-sizey+1,stepj} , {i,1+greedy\[Lambda] Mod[(j-1)/sizey,2]shift,dimx-sizex+1,stepi} ]][[2]]; If[newbricks==={},newbricks={{}}]; newbricks=bricks~Join~newbricks[[1]]; If[Length[sizes]>1, TileWithLego[newarr,Rest[sizes],greedy,newbricks] , Reverse/@newbricks ] ] CreateLegos[slice_List/;MatrixQ[slice],sizes_List,rotate:(True|False),greedy:(True|False)]:=If[rotate,Reverse/@TileWithLego[slice\[Transpose],sizes,greedy],TileWithLego[slice,sizes,greedy]] TransformLego[slices_List,bricks_List,greedy:(True|False|Automatic)]:=Module[{len,greedies,heights,rotates,brickies,brickspec}, len=Length[slices]; heights=Range[len]; rotates=(#=!=0)&/@Mod[heights,2]; greedies=Switch[greedy,True,ConstantArray[True,len],False,ConstantArray[False,len],_,Switch[len,1,{False},2,{False,False},_,{False,False}~Join~ConstantArray[True,len-2]]]; brickies=MapThread[CreateLegos[#1,bricks,#2,#3]&,{slices,rotates,greedies}]; brickspec=MapThread[{#1[[All,All,2]]-#1[[All,All,1]]+1,{#1[[All,All,1]],ConstantArray[#2,Length[#1]]}\[Transpose]}\[Transpose]&,{brickies,heights}]; brickspec=Catenate[brickspec]; brickspec[[All,2]]=Flatten/@brickspec[[All,2]]; brickies=DrawLego[#1,#2,False (* detailed *)]&@@@brickspec; {Graphics3D[{brickstyle,brickies},Boxed->False,ImageSize->700],brickspec} ] Let's turn a simple plot in to its Lego-presentation: heightmap=Table[8+Round[3.5Sin[0.1(0.1x^2+y)]/1.2],{x,-15,24,2},{y,-30,28,2}]; ListPlot3D[%,Mesh->None,InterpolationOrder->0] minmax=MinMax[heightmap]+0.5{-1,1}; slices=UnitStep[heightmap-#+1]&/@Range@@minmax; {gr,bricks}=TransformLego[slices,{{4,2},{3,2},{2,2},{4,1},{3,1},{2,1},{1,1}},Automatic]; gr giving: We can try different shapes, namely a sphere: slices=DiskMatrix[{9/dimz,9,9},20]; {gr,bricks}=TransformLego[slices,{{4,2},{3,2},{2,2},{4,1},{3,1},{2,1},{1,1}},Automatic]; gr Or a pyramid: slices=DiamondMatrix[{8,8,8},18][[10;;]]; {gr,bricks}=TransformLego[slices,{{4,2},{3,2},{2,2},{4,1},{3,1},{2,1},{1,1}},Automatic]; gr The price (according to the online lego shop), would be: prices = {{2, 4} -> 0.23, {1, 2} -> 0.11, {1, 1} -> 0.08, {1, 3} -> 0.15, {1, 4} -> 0.15, {2, 2} -> 0.15, {2, 3} -> 0.19}; Total[(Sort /@ bricks[[All, 1]]) /. prices] 18.76 We can go now and make some instructions for making this pyramid! Because I can't build something without instructions. Let's create some layer-by-layer instructions: ClearAll[CreatePage,CreatePages] CreatePage[slices_List,pagenumber_Integer]:=Module[{add,old,image,gr,gr3,opts,width=500}, {add,old}=TakeDrop[slices,-1]; image=(DrawLego[#1,#2,False]&@@@#)&/@slices; add=Flatten[add,1]; add=SortBy[Minus@*First][Reverse/@Tally[Sort/@add[[All,1]]]]; add[[All,1]]=Style[Row[{#,"\[Cross]"}],16,Black]&/@add[[All,1]]; add[[All,2]]=Graphics3D[{brickstyle,DrawLego[#]},gropts,ImageSize->50,Background->None]&/@add[[All,2]]; add=Grid[add]; gr3=Graphics3D[{brickstyle,image},Boxed->False,ViewPoint->(10{2.4, -1.3, 2.}),ImageSize->2width/3]; gr=Graphics[ {LightBlue,Rectangle[{0,0},{1,1.5}], Inset[gr3,Scaled@{0.5,0.5}], Inset[Style[ToString[pagenumber],30,Black],Scaled@{0.5,0.05},Scaled@{0.5,0}], Inset[add,Scaled@{0.05,1},Scaled@{0,1}] }, Axes->False, Frame->False, ImageSize->(width{1,1.5}), PlotRange->{{0,1},{0,1.5}}, AspectRatio->Full ] ] CreatePages[bricks_List]:=Module[{brickslices,out}, brickslices=SortBy[Part[#,1,-1,-1]&][GatherBy[bricks,Part[#,-1,-1]&]]; out = Map[CreatePage[brickslices[[;;#]],#]&,Range[Length[brickslices]]]; Rasterize[#,"Image"]& /@ out ] So let's call the function: CreatePages[bricks] gives me back 8 pages of instructions, including the bricks I need for that 'layer' !Lastly, let's make one from a 3D model: brickstyle=Sequence[RGBColor[0.55,0.38,0.19],EdgeForm[AbsoluteThickness[1]]]; bg=ExampleData[{"Geometry3D","Triceratops"},"BoundaryMeshRegion"] bounds={xbounds,ybounds,zbounds}=CoordinateBounds[ExampleData[{"Geometry3D","Triceratops"},"VertexData"]]; rmf=RegionMember[bg]; \[Delta]=2^-3; alldata=Boole[Table[rmf[{x,y,z}],{x,xbounds[[1]],xbounds[[2]],\[Delta]},{y,ybounds[[1]],ybounds[[2]],\[Delta]},{z,zbounds[[1]],zbounds[[2]],\[Delta]}]]; alldata=Transpose[alldata,{3,2,1}]; {gr,bricks}=TransformLego[alldata,{{4,2},{3,2},{2,2},{4,1},{3,1},{2,1},{1,1}},False]; gr Now feel free to turn your own plots, 3d-scans, and models to Legos!
25 Replies
Sort By:
Posted 6 years ago
 Neat! I think LEGO is a sort of manual 3D printing, using a human as a printer ;) But that leads me to another thought. If you actually 3D-print that final LEGO-beast you get a LEGO-looking thing without actually assembling it. That's cheating, I know. And potentially the Pandora box of LEGO. But for those of us loves the look but not the craft, this could be the gate to some sot of new 3D pixel art design. I would not mind at all that Dino sitting on my table.
Posted 6 years ago
 Haha! I didn't think of that! If you are lazy or don't like the assembly. It might be a viable option. My route was more on finding lego-approximations of real things...
Posted 6 years ago
 - another post of yours has been selected for the Staff Picks group, congratulations !We are happy to see you at the tops of the "Featured Contributor" board. Thank you for your wonderful contributions, and please keep them coming!
Posted 6 years ago
 This is an awesome contribution! I know a couple of little brick-meisters that would love to make that triceratops.Would it be possible to map color from the 3D model surface to standard brick colors for the assembled bricks?
Posted 6 years ago
 Thanks for the nice comment! Of course it is possible! You just need the 3d-mode, the textures and some ingenuity!! definitely not impossible!
Posted 6 years ago
 Hey! You are using SuperBond in the Horn! :)Cool post!
Posted 6 years ago
 Thanks Rodrigo!Ssssshhhh!!! Yeah sometimes you have to cheat a little, or really spend of lot of time improving the 3D-tiling algorithm. I made already quite an advanced algorithm that alternatively places them in both horizontal directions, and starts tiling with big blocks, and iteratively uses smaller bricks to fill up the gaps. As a first approximation it is not too bad, but it can certainly be improved, but might be out of scope for this post. Very thin/sharp features (like the horns) need special consideration indeed. Generally, it can be solved easily by just choosing double or quadruple the resolution ;)In the example I also didn't rescale the model in z direction by a factor of 1.2, I found out after the post... one can easily implement that...
Posted 6 years ago
 more likely using Kragle ;-)But that's a good catch, and probably easily "fixed" by using a finer resolution.
Posted 6 years ago
 Don't worry. A SuperBond 3D printer can solve that!.. :)
Posted 6 years ago
 Indeed, things that are very thin or that have a very small slope, should always 'support' the next layer, so every 3D model is not necessarily possible.. Refining will indeed help in this case, it will make thin part 'thicker'...
Posted 6 years ago
 You have done amazing job Sander, respect.
Posted 6 years ago
 Thanks Tom! Glad you liked it!
Posted 6 years ago
 Very nice!If only I had my 3D head shape, scanned during the last WTC2015...Did anyone receive it? (obviously, from the ones that had it scanned...). It would be a cool avatar! Is it easy to add brick colors matching the shape colors? (the heads were scanned "in color"...)
Posted 6 years ago
 Yes definitely possible. You just need a 3D => 2D map to get a color for a certain brick...
Posted 6 years ago
 Great contribution to the community, thanks.I encounter a problem when I call the following function: CreatePages[bricks] With Mathematica 10.4.0 for Mac OS 10.11.3 x86 (64-bit) (February 26, 2016), I only get the following output (with the number value changing on successive calls): out\$1959 I also tried with the previous version, Mathematica 10.3.x, but got the same type of output. Any comments or suggestions to try and troubleshoot would be appreciated. Thanks
Posted 6 years ago
 Hi Timothy, There is a small error in the code (now fixed above)  out = Map[CreatePage[brickslices[[;;#]],#]&,Range[Length[brickslices]]]; didn't have a semicolon at the end...Hope that works for you!
Posted 6 years ago
 Thanks Sander. That fixed it.
Posted 6 years ago
 I think the two lines hereafter don't make sense, since the % refers to the Null value returned after the ";". heightmap=Table[8+Round[3.5Sin[0.1(0.1x^2+y)]/1.2],{x,-15,24,2},{y,-30,28,2}]; ListPlot3D[%,Mesh->None,InterpolationOrder->0] I think it should probably be like: heightmap=Table[8+Round[3.5Sin[0.1(0.1x^2+y)]/1.2],{x,-15,24,2},{y,-30,28,2}]; ListPlot3D[heightmap,Mesh->None,InterpolationOrder->0] 
Posted 6 years ago
 Hi Pedro,These are equivalent in Wolfram Language, which might surprise you. When you have a command or a set of commands, separated by a semicolon it forms a 'CompoundExpression'. If you look at the help: Subscript[expr, 1];Subscript[expr, 2]; returns value Null. If it is given as input, the resulting output will not be printed. Out[n] will nevertheless be assigned to be the value of Subscript[expr, 2]. So it will set Out, despite being a semicolon. And % is just Out[]... E.g.: b = 3; % nicely gives 3; no problem!To add: Many people think ; of as "Don't print the output", which is partly what it does, but it is more complicated. It can not be compared to the Matlab ; ... because you can do things like: b = (3 + 4; 5 + 6); b (returns 11)
Posted 6 years ago
 Interesting... And yet, my 10.4 (windows), complained about it. I mean, it took Null, and not the previous expression... I haven't tried to repeat the problem, but, being it as you described, most likely this came from the fact that, after copying from the internet, there are missing returns, etc...Do you know if there's a way to import a post directly into Mathematica?
Posted 6 years ago
 Probably a copying error, it happens sometimes to me as well. Some returns or quotes get incorrect after copying. Importing code directly from a post is tricky... not sure how I would handle that, but might be possible by importing the page as an XMLObject and then looking for specific elements with the following class: "prettyprint lang-mma prettyprinted" using: Cases[xmlstuff,....,[Infinity]I've done some scraping of websites and works quite neatly if you import it symbolically.
Posted 6 years ago
 Hi Sander, this is a great post. Thanks for sharing!Recently I heard a related research coming from the SIGGRAPH Asia 2015 which I think you might be interested in (or maybe you have already read it? :)Legolization: Optimizing LEGO Designs
Posted 6 years ago
 Hi Silvia,Thanks a lot for sharing, I was unaware of this amazing video and poster!--SH