Juliaで三次元グラフィクス

## はじめに
Juliaで簡単な三次元グラフィクスを作ってみましょう。お題はルービックキューブのシミュレータです。
## パッケージ
今回使ったパッケージは以下の３個です。
GeometryBasics, GLMakie, Printf
GLMakieのインストールにはちょっと時間がかかります。
## 立方体の表示
まずは立方体を表示してみます。
頂点の座標を配列xcで指定します。立方体の頂点は8個ですが、着色の都合で各面を4個の頂点で定義し、24個の頂点で立方体を作ります。
次に、fcで指定した頂点を繋いでポリゴンを定義します。三角形のポリゴンで描くので、各面は2個ずつの三角形で、12個の三角形で立方体を描きます。
clは頂点に付く色のインデックスです。cmがカラーマップです。
FigureでGLのウインドウを開いて、Axis3で三次元の軸を作り、その軸にpoly!でポリゴンを描きます。connect は点の座標から三次元の点の列を作る関数です。
``` julia:cube1.jl
using GeometryBasics,GLMakie

# Coordinate of cube vertices (4 vertices for each faces -> 24 vertices/cube)
xc=Float32[
    -1  1 -1  1 -1  1 -1  1 -1 -1 -1 -1  1  1  1  1 -1 -1  1  1 -1 -1  1  1;
    -1 -1  1  1 -1 -1  1  1 -1  1 -1  1 -1  1 -1  1 -1 -1 -1 -1  1  1  1  1;
    -1 -1 -1 -1  1  1  1  1 -1 -1  1  1 -1 -1  1  1 -1  1 -1  1 -1  1 -1  1]
# vertices index of traiangles (12 triangles in 1 cube)
fc = [ 1  2  3;  2  3  4;  5  6  7;  6  7  8;
       9 10 11; 10 11 12; 13 14 15; 14 15 16;
      17 18 19; 18 19 20; 21 22 23; 22 23 24]
# color index of each vertices (same color for vertices in 1 face)
cl = [1, 1, 1, 1,  2, 2, 2, 2, 
      3, 3, 3, 3,  4, 4, 4, 4, 
      5, 5, 5, 5,  6, 6, 6, 6]
# color map (1-6:normal 7-12:highlight)
cm = [:white,:blue,:red,:orange,:green,:yellow]

fig=Figure(backgroundcolor=:gray,resolution=(500,500))
ax=Axis3(fig[1,1],aspect=(1,1,1))
poly!(ax, connect(reshape(xc, :), Point{3}), fc, color=cl, colormap=cm)

fig
```
## 立方体を回す
キーボードのイベントで立方体を回転させてみましょう。cube1.jl のウインドウを開く部分以降を下記のように差し替えます。
``` julia:cube2.jl
xco=Observable(xc)
xyo=lift(x->connect(reshape(x, :), Point{3}),xco)

fig=Figure(backgroundcolor=:gray,resolution=(500,500))
ax=Axis3(fig[1,1],aspect=(1,1,1))
poly!(ax, xyo, fc, color=cl, colormap=cm)

on(events(fig).keyboardbutton) do event
    if event.action == Keyboard.press &&
        ( event.key == Keyboard.left || event.key == Keyboard.right )
        r = (event.key == Keyboard.right)*2 -1
        ag = r*π/6
        rm = [cos(ag) -sin(ag) 0.;sin(ag) cos(ag) 0.;0. 0. 1.]
        xco[] .= rm * xco[]
        notify(xyo)
    end
end

fig
```
Observable で作ったオブジェクトが変更されると、それを使って描いた絵が更新されます。lift は、あるObservableオブジェクトから別のObservableを作る関数です。-> は無名関数の定義です。
on 以下がイベントハンドラです。右か左の矢印キーで立方体を回転させます。Observableオブジェクトxcoの中身にアクセスするには xco[] とします。xcoを書き変えるとxyoが更新されるのでnotifyで知らせます。
## ルービックキューブ
立方体を 3x3x3 に並べてルービックキューブを作ります。正面から見た絵と後方から見た絵を並べて描きました。下記のコードでは、回転させる面に並んだ立方体を少し明るくハイライト表示してあります。回転面を移動させるのは上下の矢印キーで、回転軸の変更は x, y, z のキーで行います。また、b で操作のundoが、p で履歴のプリントができます。
``` julia:Rubik.jl
# Rubik Cube simulator with GLMakie
using GeometryBasics,GLMakie,Printf

# Coordinate of cube vertices (4 vertices for each faces -> 24 vertices/cube)
xc=Float32[
    -1  1 -1  1 -1  1 -1  1 -1 -1 -1 -1  1  1  1  1 -1 -1  1  1 -1 -1  1  1;
    -1 -1  1  1 -1 -1  1  1 -1  1 -1  1 -1  1 -1  1 -1 -1 -1 -1  1  1  1  1;
    -1 -1 -1 -1  1  1  1  1 -1 -1  1  1 -1 -1  1  1 -1  1 -1  1 -1  1 -1  1]
# vertices index of traiangles (12 triangles in 1 cube)
fc = [ 1  2  3;  2  3  4;  5  6  7;  6  7  8;
       9 10 11; 10 11 12; 13 14 15; 14 15 16;
      17 18 19; 18 19 20; 21 22 23; 22 23 24]
# color index of each vertices (same color for vertices in 1 face)
cl = [1, 1, 1, 1,  2, 2, 2, 2, 
      3, 3, 3, 3,  4, 4, 4, 4, 
      5, 5, 5, 5,  6, 6, 6, 6]
# color map (1-6:normal 7-12:highlight)
cm = [:gray90,:blue3,:red2,:orange2,:green3,:yellow2,
      :white,:blue,:red,:orange,:green1,:yellow]
# cube index within the entire Rubik cube (3*3*3=27 cubes)
cbR=Array{Int}(undef,3,3,3)
# Coordinate of all vertices in the entire Rubik cube (24*27=648 vertices)
xcR=Array{Float64}(undef,3,648)
for k=1:3
    for j=1:3
        for i=1:3
            m = i+3*(j-1+3*(k-1))
            cbR[i,j,k] = m
            xcR[:,m*24-23:m*24] .= xc .+ [2.0*(i-2), 2.0*(j-2), 2.0*(k-2)]
        end
    end
end

# vertices index of traiangles in the entire Rubik cube (12*27=324 triangles)
fcR=Array{Int}(undef,324,3)
# color index of each vertices in the entire Rubik cube (24*27=648 vertices)
clR=Array{Int}(undef,648)
for i=1:27
    fcR[12*i-11:12*i,:] .= fc .+ (24*(i-1))
    clR[24*i-23:24*i] .= cl
end

function highlight!(cli,cbi,a,p)
    cli .= mod.((cli .- 1), 6) .+ 1
    for i=1:3
        for j=1:3
            if a == 'x'
                m=cbi[p,i,j]*24
            elseif a == 'y'
                m=cbi[i,p,j]*24
            else
                m=cbi[i,j,p]*24
            end
            cli[m-23:m].=cli[m-23:m] .+ 6
        end
    end
end     

# command history
hso=Observable(String[])
# rotation axis (x, y, z)
axo=Observable('z')
# rotation plane index (1, 2, 3)
plo=Observable(2)
# color index
clo=Observable(clR)
# cube index (observable)
cbo=Observable(cbR)
# highlight cube in the plane to rotate
highlight!(clo[],cbo[],axo[],plo[])
# vertices coordinate (observable)
xco=Observable(xcR)
# vertices 3D point object (observable)
xyo=lift(x->connect(reshape(x, :), Point{3}),xco)

fig=Figure(backgroundcolor=:gray,resolution=(1000,500))
# ax1 from front above
ax1=Axis3(fig[1,1],aspect=(1,1,1),azimuth=π/6,elevation=π/6)
# ax2 from rear below
ax2=Axis3(fig[1,2],aspect=(1,1,1),azimuth=π*7/6,elevation=-π/6)
poly!(ax1, xyo, fcR, color=clo, colormap=cm)
poly!(ax2, xyo, fcR, color=clo, colormap=cm)

function cuberot!(cb,x,a,p,r)
    # rotation angle : π/2 or -π/2
    ag = r*π/2
    if a == 'x'
        # rotation matrix
        rm = [1. 0. 0.;0. cos(ag) -sin(ag);0. sin(ag) cos(ag)]
        # rotation of cube index
        cbp = cb[p,:,:]
        for k=1:3
            for j=1:3
                cb[p,j,k] = cbp[2-r*(2-k), 2+r*(2-j)]
            end
        end
    elseif a == 'y'
        rm = [cos(ag) 0. sin(ag);0. 1. 0.;-sin(ag) 0. cos(ag)]
        cbp = cb[:,p,:]
        for k=1:3
            for i=1:3
                cb[i,p,k] = cbp[2+r*(2-k), 2-r*(2-i)]
            end
        end
    else
        rm = [cos(ag) -sin(ag) 0.;sin(ag) cos(ag) 0.;0. 0. 1.]
        cbp = cb[:,:,p]
        for j=1:3
            for i=1:3
                cb[i,j,p] = cbp[2-r*(2-j), 2+r*(2-i)]
            end
        end
    end
    # rotation of vertices coordinate
    for j=1:3
        for i=1:3
            m=cbp[i,j]*24
            x[:,m-23:m] .= rm * x[:,m-23:m]
        end
    end
end

# Observer function difinition for keyboard event
on(events(fig).keyboardbutton) do event
    if event.action == Keyboard.press
        # keyboard command definition
        # [x, y, z] : rotation axis
        # [up, down] : rotation plane up/down
        # [right, left] : direction -> rotation
        # [b] : undo
        # [p] : print operation history
        if event.key==Keyboard.x|| event.key==Keyboard.y|| 
           event.key==Keyboard.z||
           event.key==Keyboard.up|| event.key==Keyboard.down
            if event.key == Keyboard.x
                axo[] = 'x'
            elseif event.key == Keyboard.y
                axo[] = 'y'
            elseif event.key == Keyboard.z
                axo[] = 'z'
            elseif event.key == Keyboard.up
                plo[] = min(plo[]+1, 3)
            elseif event.key == Keyboard.down
                plo[] = max(plo[]-1, 1)
            end
            highlight!(clo[],cbo[],axo[],plo[])
            notify(clo)
        elseif event.key == Keyboard.left || event.key == Keyboard.right
            ro = (event.key == Keyboard.right)*2 -1
            cuberot!(cbo[],xco[],axo[],plo[],ro)
            notify(xyo)
            push!(hso[],@sprintf("%s%2i%2i",axo[],plo[],ro))
        elseif event.key == Keyboard.b
            if length(hso[])>0
                s=pop!(hso[])
                axo[]=s[1]
                plo[]=parse(Int,SubString(s,2,3))
                ro=parse(Int,SubString(s,4,5))*(-1)
                cuberot!(cbo[],xco[],axo[],plo[],ro)
                notify(xyo)
                highlight!(clo[],cbo[],axo[],plo[])
                notify(clo)
            end
        elseif event.key == Keyboard.p
            println(hso[])
        end
    end
end

fig
```
色がばらばらになってくるとハイライトした部分が見えにくくなってしまいます。この辺り改善が必要かもしれません。
## おわりに
これだけのことがこんなあっさりしたコードで書けてしまうのは本当に素晴らしいです。Enjoy!