ViewとLayerで立方体をデッサンしてみる

[昨年の球体デッサン](https://qiita.com/a_jike/items/8081b773fe73d36e4b70)に続き今年は立方体をデッサンしてみた。
今回は写真や画像を一切使わない正真正銘**完全コーディング**によるデッサンに拘った。（前回は一部写真を歪めてはめ込むチート技を使ってしまった）

結果から貼るとこうなった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/d4d93834-8505-d371-c8cb-957fbba60d72.png)

木の角材に見えるかな？
主に `CoreAnimation` `CoreGraphics` `CoreImage` を利用している。

プロジェクトは以下にアップした。
https://github.com/yumemi-ajike/Cube

* Xcode 12.2
* Swift 5.1

# 立方体の面
前面、背面、上面、底面、左側面、右側面のそれぞれをCALayerとして定義、正方形で追加する。
サイズは適当に一辺200とした。

```Swift:WireCubeView.swift
    let size: CGFloat = 200
    lazy var frontLayer: CALayer = {
        let transform = CATransform3DMakeTranslation(0, 0, size / 2)
        return createFaceLayer(with: transform)
    }()
    lazy var rightLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var topLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var leftLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(-size / 2, 0, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var bottomLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var backLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, 0, -size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    
    func createFaceLayer(with transform: CATransform3D) -> CALayer {
        
        let layer = CALayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 1
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }
```
```Swift:WireCubeView.swift
    override func layoutSubviews() {
        super.layoutSubviews()
        ...
        baseLayer.addSublayer(frontLayer)
        baseLayer.addSublayer(rightLayer)
        baseLayer.addSublayer(topLayer)
        baseLayer.addSublayer(leftLayer)
        baseLayer.addSublayer(bottomLayer)
        baseLayer.addSublayer(backLayer)
        baseLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
        
        ...
        layer.addSublayer(baseLayer)
        ...
    }
```

前面はZ方向に100移動。

```Swift
        let transform = CATransform3DMakeTranslation(0, 0, size / 2)
```

右側面はX方向に100移動、Y軸を `CGFloat.pi / 2` つまり90°回転させる。

```Swift
        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)
```

上面はY方向に-100移動、X軸を90°回転させる。

```Swift
        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
```

左側面は右側面とは逆にX方向に-100移動、Y軸を-90°回転させる。

```Swift
        var transform = CATransform3DMakeTranslation(-size / 2, 0, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 0, 1, 0)
```

底面は上面とは逆にY方向に100移動、X軸を-90°回転させる。

```Swift
        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)
```

背面はZ方向に-100移動、Y軸を `CGFloat.pi` つまり180°回転させる。

```Swift
        var transform = CATransform3DMakeTranslation(0, 0, -size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi , 0, 1, 0)
```

この時点でビルド実行すると前面（＝frontLayer）しか見えていない状態となる。
![figure1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/442791fd-7f1f-71b3-6ec4-075b9afc755e.png)

立方体として認識できる見栄えにする必要があるため、全体の角度を調整する。

```Swift
        override func layoutSubviews() {
        ...
        var transform = CATransform3DIdentity
        ...
        transform = CATransform3DRotate(transform, -30 * CGFloat.pi / 180, 0, 1, 0)
        transform = CATransform3DRotate(transform, -30 * CGFloat.pi / 180, 1, 0, 0)
        transform = CATransform3DRotate(transform, 15 * CGFloat.pi / 180, 0, 0, 1)
        baseLayer.transform = transform
```

６面全面の親である、baseLayerにtransformをかける。
Y軸を−30°、X軸を-30°、Z軸を15°傾けるとなんとか良い感じに立方体に見えなくもない。
因みに傾けると線が微妙な角度で描画されジャギって汚くなるため各レイヤーにはアンチエイリアスをかけておく。

```Swift
layer.allowsEdgeAntialiasing = true
```

![figure2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/713dcffb-1072-a290-6eb0-1d1e802aca85.png)

このままだとパースペクティブが効いていないため奥行きが感じられずどっち方向にあるのか判別できない。

```Swift
        transform.m34 = -1.0 / 1000
```

CATransform3D.m34 に値を入れることでパースがかかりどういう方向に存在しているのかわかるようになる。値は適当。
![figure3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/603252cf-3f36-1659-22b6-aca14d2334f0.png)

しっかり立方体に見えるようになった。
各面の見えるバランスも良さげ。

# 面を作る
## グラデーションで面の微妙な明暗を表現する。
見えている前面、上面、右面を `CAGradientLayer` にして色を付ける。

```Swift:CubeView.swift
    lazy var frontLayer: CAGradientLayer = {
        let transform = CATransform3DMakeTranslation(0, 0, size / 2)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0.4, alpha: 1.0),
                                                UIColor(white: 0.6, alpha: 1.0)])
    }()
    lazy var rightLayer: CAGradientLayer = {
        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0.6, alpha: 1.0),
                                                UIColor(white: 0.8, alpha: 1.0)])
    }()
    lazy var topLayer: CAGradientLayer = {
        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 1.0, alpha: 1.0),
                                                UIColor(white: 0.8, alpha: 1.0)])
    }()
```

```Swift:CubeView.swift
    func createGradientFaceLayer(with transform: CATransform3D, colors: [UIColor]) -> CAGradientLayer {
        
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.colors = colors.map { $0.cgColor }
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }
```

光源はやや右奥上にある想定で
前面は上から下まで40%→60%、
右面は上から下まで60%→80%、
上面は奥から手前まで100%→80%の白地にグレーのグラデーションがかかるようにしている。
![figure4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/eebb5e0a-1ed9-13d3-2252-983ba1859520.png)
前面と右面を下方向に明るくしているのは接地面の反射光が当たり明るくなっているという表現。


# 背景/地面を作る
`CubeView` の後ろに `GroundView` を追加する。
背景/地面（＝GroundView）は立方体（＝CubeView）とは別の階層として定義し、立方体そのものの再利用性を高めている。

```Swift:GroundView.swift
final class GroundView: UIView {
    
    lazy var groundLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.colors = [UIColor(white: 1.0, alpha: 1.0).cgColor,
                        UIColor(white: 0.7, alpha: 1.0).cgColor]
        layer.locations = [0.5, 1.0]
        return layer
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layer.addSublayer(groundLayer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        groundLayer.frame = bounds
    }
}
```

![figure5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/3a880c74-f259-fe37-2865-3deabf036386.png)



## 影を落とす
背景/地面ができたので立方体の影を落とす。
影は背景/地面 or 立方体どちらのView階層に存在すべきか議論が分かれそうだが、
影も立方体と同じパース具合にしたいのでtransformの記述が１箇所で済みそうなCubeViewのbaseLayerに追加することにした。

どうやって影を表現するか試行錯誤したけど、立方体の各面から影を作り出すことが難しかったので結局影としての面を全く別に設けることにした。

影のベースレイヤーを手前に広がるように追加する。

```Swift:CubeView.swift
    lazy var shadowLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(size / 2, size / 2, size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        let layer = CALayer()
        layer.frame = CGRect(x: -size, y: -size, width: size * 2, height: size * 2)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
```

```Swift:CubeView.swift
    override func layoutSubviews() {
        super.layoutSubviews()
        ...
        baseLayer.addSublayer(shadowLayer)
        ...
    }
```
![figure6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/59e8e5f7-1388-de07-9588-3ce1652e2565.png)

影となるグラデーションを落とす。

```Swift:CubeView.swift
    lazy var shadowGradientLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.colors = [UIColor(white: 0, alpha: 0.4), .clear].map { $0.cgColor }
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
```
```Swift:CubeView.swift
    override func layoutSubviews() {
        super.layoutSubviews()
        
        shadowLayer.addSublayer(shadowGradientLayer)
        ...
    }
```
![figure7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/02a96c56-1923-59ac-5b1b-3cda29690191.png)

影の形をパスでクリップする。

```Swift:CubeView.swift
    lazy var shadowShapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.fillColor = UIColor.black.cgColor
        let path = CGMutablePath()
        path.move(to: CGPoint(x: 0, y: size))
        path.addLine(to: CGPoint(x: size, y: size))
        path.addLine(to: CGPoint(x: size, y: 0))
        path.addLine(to: CGPoint(x: size * 1.5, y: size))
        path.addLine(to: CGPoint(x: size / 2 * 3, y: size * 2))
        path.addLine(to: CGPoint(x: size / 2, y: size * 2))
        path.addLine(to: CGPoint(x: 0, y: size))
        path.closeSubpath()
        layer.path = path
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
```
```Swift:CubeView.swift
    override func layoutSubviews() {
        super.layoutSubviews()
        
        ...
        shadowGradientLayer.mask = shadowShapeLayer
        ...
```
![figure9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/baeea038-7a10-b585-4e77-b43f4fb1c622.png)

グラデーションに対してこのようなパスでクリップすることで影にみせかける。
![figure8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/3cd8979a-20d2-a965-a540-94543914479d.png)

それらしくなっているが何の立方体かわからないし、リアルさにかけるためCG丸出し感が否めないので詳細を作り込んでいく。

# 形状の作り込み
立方体の形状などディティールを追求していくことで画としての説得力を上げていく。

## 角を落とす
各面同士が直角に接している部分が現実の物体ではありえないほど鋭い。
この時点ではまだ素材は明らかになっていないが現実のものなら金属でも木材でもプラスティックでも必ずわずかな角の丸みがあるはずなので角を落としていく。

角を丸めて内容をクリップしたいため `CAGradientLayer` を `CALayer` のsublayerにして、`cornerRadius` を設定する。

```Swift
    let cornerRadius: CGFloat = 2.0
    ...

    func createGradientFaceLayer(with transform: CATransform3D, colors: [UIColor]) -> CALayer {
        
        let layer = CALayer()
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = CGRect(x: 0, y: 0, width: size, height: size)
        gradientLayer.colors = colors.map { $0.cgColor }
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.cornerRadius = cornerRadius
        layer.masksToBounds = true
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        layer.addSublayer(gradientLayer)
        return layer
    }
```

次に前面/上面/右側のそれぞれが接する部分にハイライトとして白→透明のグラデーションを置く。

```Swift
    lazy var frontTopLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
        layer.colors = [UIColor(white: 1, alpha: 0.8),
                        UIColor(white: 1, alpha: 0)].map { $0.cgColor }
        layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontLeftLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        let transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
        layer.colors = [UIColor(white: 1, alpha: 0.3),
                        UIColor(white: 1, alpha: 0)].map { $0.cgColor }
        layer.startPoint = CGPoint(x: 0, y: 0.5)
        layer.endPoint = CGPoint(x: 1, y: 0.5)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontRightLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        let transform = CATransform3DMakeTranslation(size * 1.5 - cornerRadius, size / 2, 0)
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
        layer.colors = [UIColor(white: 1, alpha: 0),
                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
        layer.startPoint = CGPoint(x: 0, y: 0.5)
        layer.endPoint = CGPoint(x: 1, y: 0.5)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontBottomLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
        layer.colors = [UIColor(white: 1, alpha: 0),
                        UIColor(white: 1, alpha: 0.2)].map { $0.cgColor }
        layer.transform = CATransform3DMakeTranslation(size / 2, size * 1.5 - cornerRadius, 0)
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
```

```Swift
    override func layoutSubviews() {
        ...
        frontLayer.addSublayer(frontTopLayer)
        frontLayer.addSublayer(frontRightLayer)
        frontLayer.addSublayer(frontLeftLayer)
        frontLayer.addSublayer(frontBottomLayer)
        rightLayer.addSublayer(rightTopLayer)
        rightLayer.addSublayer(rightLeftLayer)
        rightLayer.addSublayer(rightRightLayer)
        rightLayer.addSublayer(rightBottomLayer)
        topLayer.addSublayer(topTopLayer)
        topLayer.addSublayer(topBottomLayer)
        topLayer.addSublayer(topRightLayer)
        topLayer.addSublayer(topLeftLayer)
        ...
    }
```

cornerRadiusを大袈裟な値にするとこんな感じでサイコロのようになる。
![figure10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/8eb70b56-7ec5-76e0-97e5-530f884b91ff.png)

立方体の形状と影が合わないため、立方体に合った影の形に `shadowShapeLayer` のパスを修正する。

```Swift
    lazy var shadowShapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.fillColor = UIColor.black.cgColor
        let path = CGMutablePath()
        path.move(to: CGPoint(x: cornerRadius, y: size))
        path.addLine(to: CGPoint(x: size - cornerRadius, y: size))
        // curve
        path.addCurve(to: CGPoint(x: size, y: size - cornerRadius), control1: CGPoint(x: size, y: size), control2: CGPoint(x: size, y: size - cornerRadius))
        path.addLine(to: CGPoint(x: size, y: cornerRadius))
        // curve
        path.addCurve(to: CGPoint(x: size + cornerRadius, y: cornerRadius * 2), control1: CGPoint(x: size, y: 0), control2: CGPoint(x: size + cornerRadius, y: cornerRadius * 2))
        path.addLine(to: CGPoint(x: size * 1.5, y: size))
        path.addLine(to: CGPoint(x: size / 2 * 3, y: size * 2))
        path.addLine(to: CGPoint(x: size / 2, y: size * 2))
        path.addLine(to: CGPoint(x: cornerRadius, y: size + cornerRadius * 2))
        // curve
        path.addCurve(to: CGPoint(x: cornerRadius, y: size), control1: CGPoint(x: 0, y: size), control2: CGPoint(x: cornerRadius, y: size))
        path.closeSubpath()
        layer.path = path
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
```

![figure11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/c7bd3638-e075-a024-a0ed-f6b6c36ad166.png)


角丸の値を小さくして目立たなくする。
![figure12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a074d981-ffa8-3824-c02f-bd516b40756b.png)


## 接地面の影
立方体の接地面の影をしっかり描くことで物体がある面に接地していることをより表現できる上に、画にパシッと締りができる。

立方体の底面である `bottomLayer` にshadowを設定することで実現してみる。。。
![figure13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/0143f122-69b1-56af-f948-ea6e065c502a.png)

一見、おかしくないようだがよく見ると画面左端の影の落ち方がおかしい。
また角を丸めているはずなのに影が濃すぎるという問題も浮かび上がる。
![figure13-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/6617c56d-2861-2cb0-52c3-c4e9fd7de596.png)

`bottomLayer` のsublayerに影の対象となる面 `shadowBaseLayer` を追加し背景色をつけることで`bottomLayer` にshadowを設定した際、この `shadowBaseLayer` に対してのみ影がかかるようになる。

```Swift
    lazy var bottomLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)
        let layer = createFaceLayer(with: transform, color: .clear)
        let shadowRadius: CGFloat = 3
        let shadowBaseLayer = CALayer()
        shadowBaseLayer.frame = CGRect(x: cornerRadius, y: cornerRadius, width: size - cornerRadius * 2, height: size - cornerRadius * 2)
        shadowBaseLayer.backgroundColor = UIColor.white.cgColor
        layer.addSublayer(shadowBaseLayer)
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowRadius = shadowRadius
        layer.shadowOpacity = 1
        layer.shadowOffset = CGSize(width: 1, height: -2)
        return layer
    }()
```

`shadowBaseLayer` は `bottomLayer` の矩形に対して角丸分だけ内側にレイアウトされるようにすることで底面の周囲が接地していない表現を行う。台に置かれたサイコロの周囲がぴったり接地していないのと同じ。
![figure14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a78a672f-0c3c-a03f-771b-339e48b73838.png)
![figure15-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/54daf2d0-03c6-114e-17b0-c2f66a88eddf.png)

一旦、作り込み前後の比較

|ハイライト/影なし|ハイライト/影あり|
|---|---|
|![figure15-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/72809817-be08-e769-f2e1-2151235ceda0.png)|![figure15-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/54daf2d0-03c6-114e-17b0-c2f66a88eddf.png)|

画のリアル感はやや向上した。形状や状態がより伝わりやすくもなったが、如何せん何でできているのか素材や質感が全くわからないのでデッサンとしてはNGもう一歩。


# 素材や質感
立方体の素材を描くことによって質感を表現し、さらに説得力を上げていく。

ガラス製だと立方体が置かれている周りの風景の映り込みを表現する必要があり、これをコードのみで表現するのは難しそう。また前回のように写真を使ってしまいそうだ。
プラスティック製も表面がツルツルしているため、映り込みの表現が必要だしなんか味気ない。
金属製も同じく。
石膏は表面についた傷や汚れなどを表現する必要があり、これもコードで表現するのは過酷。

木ならどうか。
年輪はある程度規則性があるので表現できるかもしれないということで角材の木目を描いてみることにした。

## 準備

まずは各面にテクスチャを貼る準備。
各面のsublayerに加えたグラデーションの色指定をグレースケールから白のアルファでの表現に変更して、
各面のCALayer.contentsに `CGImage` を入れられるようにする。

```Swift:CubeView.swift
    lazy var frontLayer: CALayer = {
        ...
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0, alpha: 0.6),
                                                UIColor(white: 0, alpha: 0.4)])
    }()
```

## テクスチャの作成
`LumberTexture` クラスを追加。
木の色や年輪の間隔、幅、濃さ、色を管理し、最終的にCGImageを吐くオブジェクト。
吐いたImageを各レイヤーにセットする寸法。

設計的には
立方体の角度や陰影は `CubeView` が管理していて、`LumberTexture` はテクスチャ情報のみを管理することになるので本来は６面すべてのイメージを生成するオブジェクトでなければならないが、時間の都合で見えている３面だけにした。

```Swift:LumberTexture.swift
final class LumberTexture {
    // 年輪
    struct Ring {
        let distance: CGFloat          // 間隔
        let width: CGFloat             // 幅
        let depth: CGFloat             // 濃さ
        let colorComponents: [CGFloat] // 色
    }
    // １辺の長さ
    let side: CGFloat
    // 木の色
    let baseColorComponents: [CGFloat] = [(226 / 255), (193 / 255), (146 / 255)]
    // 細かい年輪の色 RGB
    let smoothRingColorComponents: [CGFloat] = [(199 / 255), (173 / 255), (122 / 255)]
    // 荒い年輪の色 RGB
    let roughRingColorComponents: [[CGFloat]] = [
        [(176 / 255), (106 / 255), (71 / 255)],
        [(194 / 255), (158 / 255), (96 / 255)],
    ]
    // 細かい年輪の情報
    private var roughRings: [Ring] = []
    // 荒い年輪の情報
    private var smoothRings: [Ring] = []
}
```

### 年輪の情報生成
細かい年輪は狭い感覚/細い幅で対角線の長さ分だけランダムに用意する。
色は木の色に対してあまり目立たない色を指定している。
色は適当にググって出てきた檜材からサンプリングしたRGB。

```Swift:LumberTexture.swift
    private func createSmoothRings() -> [Ring] {
        
        var smoothRings: [Ring] = []
        var pointer: CGFloat = 0
        
        repeat {
            let distance = CGFloat(Float.random(in: 2 ... 3))
            let width = CGFloat(Float.random(in: 0.5 ... 2))
            let depth = CGFloat(Float.random(in: 0.8 ... 1.0))
            let colorComponents = smoothRingColorComponents
            if (pointer + distance + width / 2) < (side * sqrt(2)) {
                smoothRings.append(Ring(distance: distance, width: width, depth: depth, colorComponents: colorComponents))
                pointer += distance
            } else {
                break
            }
            
        } while(pointer < (side * sqrt(2)))
        
        return smoothRings
    }
```

荒い年輪は広い間隔/太い幅も入るようにして木の色に対して目立つ色を指定している。

```Swift:LumberTexture.swift
    private func createRoughRings() -> [Ring] {
        
        var roughRings: [Ring] = []
        var pointer: CGFloat = 0
        
        repeat {
            let distance = CGFloat(Float.random(in: 5 ... 30))
            let width = CGFloat(Float.random(in: 2 ... 12))
            let depth = CGFloat(Float.random(in: 0.4 ... 0.6))
            let colorComponents = roughRingColorComponents[Int.random(in: 0 ... 1)]
            if (pointer + distance + width / 2) < (side * sqrt(2)) {
                roughRings.append(Ring(distance: distance, width: width, depth: depth, colorComponents: colorComponents))
                pointer += distance
            } else {
                break
            }
            
        } while(pointer < (side * sqrt(2)))
        
        return roughRings
    }
```

上面と側面が別のイメージになるがそれぞれ同じ年輪幅でないと辻褄が合わなくなってくるため年輪の情報を保持している。
イメージの取得funcを用意してこんな感じでパスで年輪を描く。

### 上面のイメージ

```Swift:LumberTexture.swift
func lumberTopImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        context.setFillColor(UIColor(red: baseColorComponents[0],
                                     green: baseColorComponents[1],
                                     blue: baseColorComponents[2],
                                     alpha: 1).cgColor)
        context.fill(CGRect(x: 0, y: 0, width: side, height: side))
        
        // Draw annual tree rings
        [smoothRings, roughRings].forEach { rings in
            var pointer: CGFloat = 0
            rings.forEach { ring in
                pointer += ring.distance
                
                context.setLineWidth(ring.width)
                let startPoint = CGPoint(x: pointer, y: side)
                let endPoint = CGPoint(x: 0, y: side - pointer)
                context.move(to: startPoint)
                context.addCurve(to: endPoint,
                                  control1: CGPoint(x: pointer, y: side - pointer),
                                  control2: endPoint)
                let components: [CGFloat] = ring.colorComponents
                context.setStrokeColor(UIColor(red: components[0],
                                               green: components[1],
                                               blue: components[2],
                                               alpha: ring.depth).cgColor)
                context.strokePath()
            }
        }
        
        return context.makeImage()
    }
```

ランダムで生成しているので毎回異なる模様になるけど大体上面のイメージはこんな感じになる。
バームクーヘン。
![top_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/fa8784c6-ac16-49c0-db69-9bc33fb22575.png)

### 前面のイメージ
前面にくる木目は上面のこの部分を引き伸ばしたものが垂直に落ちる形になる。
![figure17-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/9837a367-f11d-68f6-3609-454715341f31.png)

保持している年輪の情報から前面の木目を描く。

```Swift:LumberTexture.swift
    func lumberSideImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side * sqrt(2), height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        context.setFillColor(UIColor(red: baseColorComponents[0],
                                     green: baseColorComponents[1],
                                     blue: baseColorComponents[2],
                                     alpha: 1).cgColor)
        context.fill(CGRect(x: 0, y: 0, width: side * sqrt(2), height: side))
        
        // Draw smooth annual tree rings
        var pointer: CGFloat = 0
        smoothRings.forEach { ring in
            pointer += ring.distance
            
            context.setLineWidth(ring.width)
            let startPoint = CGPoint(x: pointer, y: 0)
            let endPoint = CGPoint(x: pointer, y: side)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let components: [CGFloat] = ring.colorComponents
            context.setStrokeColor(UIColor(red: components[0],
                                           green: components[1],
                                           blue: components[2],
                                           alpha: ring.depth).cgColor)
            context.strokePath()
        }
        
        // Draw rough annual tree rings
        pointer = 0
        roughRings.forEach { ring in
            pointer += ring.distance
            
            context.setLineWidth(ring.width)
            let startPoint = CGPoint(x: pointer, y: 0)
            let endPoint = CGPoint(x: pointer, y: side)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let components: [CGFloat] = ring.colorComponents
            context.setStrokeColor(UIColor(red: components[0],
                                           green: components[1],
                                           blue: components[2],
                                           alpha: ring.depth).cgColor)
            context.strokePath()
        }
        
        ...
    }
```

上面と同じ年輪情報で、上から下に描画する。
![figure17-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/3ee72f3e-e318-3ef1-0602-8cb85de8915e.png)

但し、人工物のように全ての年輪が並行/垂直に描かれるのは不自然なため、
`CIFilter` の `CITwirlDistortion` を使って少し歪める。

```Swift:LumberTexture.swift
    func lumberSideImage() -> CGImage? {
        
        ...
        
        // Distort the pattern
        if let image = context.makeImage() {
            
            let ciimage = CIImage(cgImage: image)
            let filter = CIFilter(name: "CITwirlDistortion")
            filter?.setValue(ciimage, forKey: kCIInputImageKey)
            filter?.setValue(CIVector(x: side * 1.2, y: -side / 3), forKey: kCIInputCenterKey)
            filter?.setValue(side * 1.3, forKey: kCIInputRadiusKey)
            filter?.setValue(CGFloat.pi / 8, forKey: kCIInputAngleKey)
            
            if let outputImage = filter?.outputImage {
                let cicontext = CIContext(options: nil)
                return cicontext.createCGImage(outputImage, from: CGRect(x: 0, y: 0, width: side, height: side))
            }
            
            return image
        }
        return nil
    }
```

![side_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/fd59d143-f482-0e5f-9d73-c9ad7f5531f8.png)

上面と前面にイメージをはめてみる。
![figure17-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/2854f36d-3fb9-f9d5-afc1-d598a905646d.png)

### 右側面のイメージ
ここの表現が非常に難しい。。
上面と前面の年輪の断面を同時に表現する必要があってさらに前面には歪みも設けているため、全てパスで表現すると相当複雑な計算が必要になると思われる。
知識もないし工数もないのでなんとか試行錯誤して誤魔化すことにした。

まずは上面の断面である年輪を描く。
ここから続く模様を生成しなければならない。
![figure18-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/5852400f-6cd4-81a4-e380-0b24c3f9c91f.png)

上面イメージの右端1pxからタイリングして90°回転したイメージを作り出す。

```Swift:LumberTexture.swift
    private func topTilingImage(topImage: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let cropImage = topImage.cropping(to: CGRect(x: side - 1, y: 0, width: 1, height: side)) {
            
            context.saveGState()
            context.rotate(by: -CGFloat.pi / 2)
            context.draw(cropImage, in: CGRect(x: 0, y: 0, width: side, height: side), byTiling: true)
            context.restoreGState()
            
            return context.makeImage()
        }
        return nil
    }
```

![figure18-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/278abb68-68ad-2b5a-d453-19097ced9637.png)

次に前面の断面である年輪を描く。
ここから続く模様を生成しなければならないが、歪めた影響でやや複雑。
![figure18-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/e3b90f44-854f-d6f3-fc1a-4c8697cfe53e.png)

こちらも同じくイメージの右端1pxからタイリングしてイメージを作り出す。

```Swift:LumberTexture.swift
    private func sideTilingImage(sideImage: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let cropImage = sideImage.cropping(to: CGRect(x: side - 1, y: 0, width: 1, height: side)) {
            
            context.saveGState()
            context.draw(cropImage, in: CGRect(x: 0, y: 0, width: side, height: side), byTiling: true)
            context.restoreGState()
            return context.makeImage()
        }
        return nil
    }
```

![figure18-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/03ae74ee-94e0-8800-a3df-28263d5ba93e.png)


|上断面に合わせると|前断面に合わせると|
|---|---|
|![figure18-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/62236682-1d78-7e58-d477-785bed08a38a.png)|![figure18-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/5e6ac3a7-b0fc-25a1-4006-170c812fc1c0.png)|
|一見良さそうだが、前面の歪み部分と合ってこない。。|前面の歪みから続く部分はこちらで良さそうだが。。|

3D的に考えると前断面は少しで、あとは基本 上断面模様になるはず？こんな感じ？やや不自然だがまあいいか。
![stretch_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/f7ca5713-9b3b-8b8e-e977-0220570b271e.png)

上断面イメージにグラデーションマスクした前断面イメージを合成する。

```Swift:LumberTexture.swift
    func lumberStrechImage(topImage: CGImage?, sideImage: CGImage?) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let topImage = topImage,
           let tilingImage = topTilingImage(topImage: topImage) {
            
            context.saveGState()
            context.draw(tilingImage, in: CGRect(x: 0, y: 0, width: side, height: side))
            context.restoreGState()
        }
        
        if let sideImage = sideImage,
            let tilingImage = sideTilingImage(sideImage: sideImage),
            let maskedImage = gradientMaskedImage(image: tilingImage) {
            
            context.saveGState()
            context.draw(maskedImage, in: CGRect(x: 0, y: 0, width: side, height: side))
            context.restoreGState()
        }
        
        return context.makeImage()
    }
```

```Swift:LumberTexture.swift
    private func gradientMaskedImage(image: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceGray(),
                                     colors: [UIColor.black.cgColor,
                                              UIColor.white.cgColor] as CFArray,
                                     locations: [0.8, 1.0]) {

            context.saveGState()
            context.drawLinearGradient(gradient,
                                       start: CGPoint(x: 0, y: 0),
                                       end: CGPoint(x: side / 4, y: side / 8),
                                       options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
            context.restoreGState()
            if let maskImage = context.makeImage(),
               let mask = CGImage(maskWidth: maskImage.width,
                                  height: maskImage.height,
                                  bitsPerComponent: maskImage.bitsPerComponent,
                                  bitsPerPixel: maskImage.bitsPerPixel,
                                  bytesPerRow: maskImage.bytesPerRow,
                                  provider: maskImage.dataProvider!,
                                  decode: nil,
                                  shouldInterpolate: false) {
                
                return image.masking(mask)
            }
        }
        return nil
    }
```

あまり自信ないけどこんな感じで誤魔化した。
![figure18-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/6121d442-8dd9-6d2c-283a-f1da570657bd.png)

## 木目の保存
木目の描画は毎回ランダムで別の幅や色太さになる。
気に入った木目を継続できるようにすれば作り込み時の比較にも一役買いそう。

`Ring` 構造体をCodableにする。

```Swift:LumberTexture.swift
final class LumberTexture {
    struct Ring: Codable {
        let distance: CGFloat
        let width: CGFloat
        let depth: CGFloat
        let colorComponents: [CGFloat]
    }
    ...
```

年輪の情報を保持している部分に保存機能を付ける。
`Ring` をCodableにしたため、JSONDecoder/JSONEncoderを使ってそのままUserDefaultsに読み書きする。

```Swift:LumberTexture.swift
    private func createSmoothRings() -> [Ring] {
        
        // Restore saved rings from UserDefaults
        if let data = UserDefaults.standard.data(forKey: "SmoothRings"),
              let rings = try? JSONDecoder().decode([Ring].self, from: data) {
            
            return rings
        }
        
        // 生成処理
        
        // Save rings to UserDefaults
        UserDefaults.standard.setValue(try? JSONEncoder().encode(smoothRings), forKeyPath: "SmoothRings")
        
        return smoothRings
    }
```

タップした時にのみ木目を作り直すようにする。

```Swift:CubeView.swift
    override func layoutSubviews() {
        super.layoutSubviews()
        
        ...
        
        updateTexture()
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(updateTextureAction)))
    }
```

```Swift:CubeView.swift
    private func updateTexture() {
        
        let topImage = texture.lumberTopImage()
        let sideImage = texture.lumberSideImage()
        topLayer.contents = topImage
        frontLayer.contents = sideImage
        rightLayer.contents = texture.lumberStrechImage(topImage: topImage, sideImage: sideImage)
    }
    
    @objc private func updateTextureAction() {
        
        texture.updateRings()
        updateTexture()
    }
```

```Swift:LumberTexture.swift
final class LumberTexture {

    ...
    
    func updateRings() {
        
        UserDefaults.standard.removeObject(forKey: "SmoothRings")
        UserDefaults.standard.removeObject(forKey: "RoughRings")
        self.smoothRings = createSmoothRings()
        self.roughRings = createRoughRings()
    }
}
```

これでタップしない限り木目が更新されない形になる。

## ハイライトの調整
![figure19-0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/5556e4c4-b53c-e71f-7904-176d6770e7bf.png)
上面に接する部分のハイライトが強すぎるため明度調整。

```diff
--- a/Cube/CubeView.swift
+++ b/Cube/CubeView.swift
@@ -65,7 +65,7 @@ final class CubeView: UIView {
     lazy var frontTopLayer: CAGradientLayer = {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
-        layer.colors = [UIColor(white: 1, alpha: 0.8),
+        layer.colors = [UIColor(white: 1, alpha: 0.3),
                         UIColor(white: 1, alpha: 0)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
         layer.allowsEdgeAntialiasing = true
@@ -107,7 +107,7 @@ final class CubeView: UIView {
     lazy var rightTopLayer: CAGradientLayer = {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
-        layer.colors = [UIColor(white: 1, alpha: 0.8),
+        layer.colors = [UIColor(white: 1, alpha: 0.3),
                         UIColor(white: 1, alpha: 0)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
         layer.allowsEdgeAntialiasing = true
@@ -172,7 +172,7 @@ final class CubeView: UIView {
         let transform = CATransform3DMakeTranslation(size * 1.5 - cornerRadius, size / 2, 0)
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
         layer.colors = [UIColor(white: 1, alpha: 0),
-                        UIColor(white: 1, alpha: 1)].map { $0.cgColor }
+                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
         layer.startPoint = CGPoint(x: 0, y: 0.5)
         layer.endPoint = CGPoint(x: 1, y: 0.5)
         layer.transform = transform
@@ -183,7 +183,7 @@ final class CubeView: UIView {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
         layer.colors = [UIColor(white: 1, alpha: 0),
-                        UIColor(white: 1, alpha: 1)].map { $0.cgColor }
+                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size * 1.5 - cornerRadius, 0)
         layer.allowsEdgeAntialiasing = true
         return layer
```

![figure19-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/3f73c35c-00c7-2290-a364-d7f1b036d7ff.png)

グレースケールにして確認してみる。

```diff:grayscale.diff
diff --git a/Cube/LumberTexture.swift b/Cube/LumberTexture.swift
index a5cdbb4..1df3b2c 100644
--- a/Cube/LumberTexture.swift
+++ b/Cube/LumberTexture.swift
@@ -116,7 +116,7 @@ extension LumberTexture {
         context.setFillColor(UIColor(red: baseColorComponents[0],
                                      green: baseColorComponents[1],
                                      blue: baseColorComponents[2],
-                                     alpha: 1).cgColor)
+                                     alpha: 1).convertToGrayScaleColor().cgColor)
         context.fill(CGRect(x: 0, y: 0, width: side, height: side))
         
         // Draw annual tree rings
@@ -136,7 +136,7 @@ extension LumberTexture {
                 context.setStrokeColor(UIColor(red: components[0],
                                                green: components[1],
                                                blue: components[2],
-                                               alpha: ring.depth).cgColor)
+                                               alpha: ring.depth).convertToGrayScaleColor().cgColor)
                 context.strokePath()
             }
         }
@@ -153,7 +153,7 @@ extension LumberTexture {
         context.setFillColor(UIColor(red: baseColorComponents[0],
                                      green: baseColorComponents[1],
                                      blue: baseColorComponents[2],
-                                     alpha: 1).cgColor)
+                                     alpha: 1).convertToGrayScaleColor().cgColor)
         context.fill(CGRect(x: 0, y: 0, width: side * sqrt(2), height: side))
         
         // Draw smooth annual tree rings
@@ -170,7 +170,7 @@ extension LumberTexture {
             context.setStrokeColor(UIColor(red: components[0],
                                            green: components[1],
                                            blue: components[2],
-                                           alpha: ring.depth).cgColor)
+                                           alpha: ring.depth).convertToGrayScaleColor().cgColor)
             context.strokePath()
         }
         
@@ -188,7 +188,7 @@ extension LumberTexture {
             context.setStrokeColor(UIColor(red: components[0],
                                            green: components[1],
                                            blue: components[2],
-                                           alpha: ring.depth).cgColor)
+                                           alpha: ring.depth).convertToGrayScaleColor().cgColor)
             context.strokePath()
         }
         
@@ -301,3 +301,12 @@ extension LumberTexture {
         return nil
     }
 }
+
+extension UIColor {
+    func convertToGrayScaleColor() -> UIColor {
+        var grayscale: CGFloat = 0
+        var alpha: CGFloat = 0
+        self.getWhite(&grayscale, alpha: &alpha)
+        return UIColor(white: grayscale, alpha: alpha)
+    }
+}
```

![grayscale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a8e59fe8-226f-b38b-7843-61ccccab0375.png)
明度の問題はなさそう。
もう少し木目のリアル感を上げ作り込みたい。

## 木目のリアル感を向上させる
木目のベースとなる色の幅が単色なためか単調な印象があるのと、少し色が暗い感じがするので色数を増やす。

```Swift:LumberTexture.swift
final class LumberTexture {
    ...
    let baseColorComponents: [CGFloat] = [(255 / 255), (227 / 255), (220 / 255)]
    let centerBaseColorComponents: [[CGFloat]] = [
        [(205 / 255), (175 / 255), (131 / 255)],
        [(201 / 255), (138 / 255), (40 / 255)],
    ]
    ...
```

上面の年輪描画部分でベースとなる描画部分に80%の幅で２色のグラデーションを追加して自然に見せる。

```Swift:LumberTexture.swift
    func lumberTopImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        ...
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                     colors: [
                                        UIColor(red: centerBaseColorComponents[0][0],
                                                green: centerBaseColorComponents[0][1],
                                                blue: centerBaseColorComponents[0][2],
                                                alpha: 0.3).cgColor,
                                        UIColor(red: centerBaseColorComponents[1][0],
                                                green: centerBaseColorComponents[1][1],
                                                blue: centerBaseColorComponents[1][2],
                                                alpha: 1).cgColor] as CFArray,
                                     locations: [0.7, 1.0]) {
            
            context.drawRadialGradient(gradient,
                                       startCenter: CGPoint(x: 0, y: side),
                                       startRadius: 0,
                                       endCenter: CGPoint(x: 0, y: side),
                                       endRadius: side * 0.8,
                                       options: [.drawsBeforeStartLocation])
        }
        ...
    }
```

上面の変更に追随するように前面の描画も併せて変更する。

```Swift:LumberTexture.swift
    func lumberSideImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side * sqrt(2), height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        ...
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                     colors: [
                                        UIColor(red: centerBaseColorComponents[0][0],
                                                green: centerBaseColorComponents[0][1],
                                                blue: centerBaseColorComponents[0][2],
                                                alpha: 0.3).cgColor,
                                        UIColor(red: centerBaseColorComponents[1][0],
                                                green: centerBaseColorComponents[1][1],
                                                blue: centerBaseColorComponents[1][2],
                                                alpha: 1).cgColor] as CFArray,
                                     locations: [0.7, 1.0]) {
            
            context.drawLinearGradient(gradient,
                                       start: CGPoint.zero,
                                       end: CGPoint(x: side * 0.8, y: 0),
                                       options: [.drawsBeforeStartLocation])
        }
        ...
    }
```

太い年輪の一部の色が目立ちすぎているため色味を調整する

```Swift:LumberTexture.swift
    let roughRingColorComponents: [[CGFloat]] = [
        [(176 / 255), (130 / 255), (71 / 255)], <-
        [(194 / 255), (158 / 255), (96 / 255)],
    ]
```

|上面|前面|右側面|
|---|---|---|
|![figure20-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/8d77daeb-139a-863b-41b3-0b9d6bedce59.png)|![figure20-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/7858c4db-d5f7-4574-71ec-9478b9dd2ae6.png)|![figure20-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a0fe4132-3e7d-6288-d986-8eb37d6ffe68.png)|

|Before|After|
|---|---|
|![figure20-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/cd80bcfe-528e-4c49-1e22-cc6c7ed86715.png)|![figure20-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/73f33c28-3f1d-de50-7592-a708672828ed.png)|

* 基本色が明るくなった
* 細かい年輪がより目立つようになった
* 木目の色に幅ができた

## 切断跡
木材をよく観察すると木の繊維とは別の加工する際についた傷が残っていることがある。
木の繊維に逆らう方向に切ることにより後が残ると思われる。
やや人工的に等間隔で描く。

```Swift:LumberTexture.swift
    func lumberTopImage() -> CGImage? {
        
        ...
        
        // Draw scratch
        var pointer: CGFloat = 0
        repeat {
            
            context.setLineWidth(1)
            let startPoint = CGPoint(x: 0, y: pointer * sqrt(2))
            let endPoint = CGPoint(x: pointer * sqrt(2), y: 0)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let alpha = (1 - pointer / side * sqrt(2))
            context.setStrokeColor(UIColor(white: 1, alpha: alpha).cgColor)
            context.strokePath()
            
            pointer += 6
        } while(pointer < side * sqrt(2))
        
        return context.makeImage()
    }
```

上面の奥から手前に徐々に効果がなくなるように描画。
わかりやすく赤で描画するとこんな感じ。

![figure20-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/4468939b-1a21-5754-fbad-8d70ec240e85.png)

|Before|After|
|---|---|
|![figure20-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/73f33c28-3f1d-de50-7592-a708672828ed.png)|![figure20-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/c0f90bf9-3ef6-ebc5-2d93-525e95ff58bc.png)|

効果は高くないが上面に色幅と新たな方向が加わった。

# 遊ぶ
## タップして木目を切り替える
![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a1ddb698-2a83-ba15-17cc-d519d487fa02.gif)

## 色情報の定義を分離する
色情報を外から入れられるようにすることでカスタム可能な作りにする。
構造体として `LumberColorSet ` を定義して指定できるようにする。

```Swift:LumberColorSet.swift
struct LumberColorSet {
    let baseColorComponents: [CGFloat]
    let centerBaseColorComponents: [[CGFloat]]
    let smoothRingColorComponents: [CGFloat]
    let roughRingColorComponents: [[CGFloat]]
    
    static var `default`: LumberColorSet {
        return .init(baseColorComponents: [
                        (255 / CGFloat(255)), (227 / CGFloat(255)), (220 / CGFloat(255))
                     ],
                     centerBaseColorComponents: [
                        [(205 / CGFloat(255)), (175 / CGFloat(255)), (131 / CGFloat(255))],
                        [(201 / CGFloat(255)), (138 / CGFloat(255)), (40 / CGFloat(255))],
                     ],
                     smoothRingColorComponents: [
                        (199 / CGFloat(255)), (173 / CGFloat(255)), (122 / CGFloat(255))
                     ],
                     roughRingColorComponents: [
                        [(176 / CGFloat(255)), (130 / CGFloat(255)), (71 / CGFloat(255))],
                        [(194 / CGFloat(255)), (158 / CGFloat(255)), (96 / CGFloat(255))],
                     ])
    }
}
```

```Swift:LumberTexture.swift
final class LumberTexture {
    ...
    private var colorSet: LumberColorSet = LumberColorSet.default
    
    init(side: CGFloat, colorSet: LumberColorSet = LumberColorSet.default) {
        self.side = side
        self.base = createBase()
        self.smoothRings = createSmoothRings()
        self.roughRings = createRoughRings()
    }
```

## クリスマスカラー
クリスマスカラーを定義してみる。

```Swift:LumberColorSet.swift
struct LumberColorSet {
    ...
    static var xmas: LumberColorSet {
        return .init(baseColorComponents: [
                        (255 / CGFloat(255)), (245 / CGFloat(255)), (193 / CGFloat(255))
                     ],
                     centerBaseColorComponents: [
                        [(105 / CGFloat(255)), (58 / CGFloat(255)), (24 / CGFloat(255))],
                        [(223 / CGFloat(255)), (176 / CGFloat(255)), (39 / CGFloat(255))],
                     ],
                     smoothRingColorComponents: [
                        (0 / CGFloat(255)), (162 / CGFloat(255)), (95 / CGFloat(255))
                     ],
                     roughRingColorComponents: [
                        [(160 / CGFloat(255)), (28 / CGFloat(255)), (34 / CGFloat(255))],
                        [(255 / CGFloat(255)), (0 / CGFloat(255)), (0 / CGFloat(255))],
                     ])
    }
    ...
}
```

`#FFF5C1` `#693A18` `#DFB027` `#00A25F` `#A01C22` `#FF0000`
いい感じの模様になるまでタップを繰り返す。なんかちょっと怖いかも。スイカっぽいし。
![figure21-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/362326ee-236b-3145-8b57-2a03ddd0c0ec.png)

## 画像から色を抽出
RGBを定義するのが面倒なので画像から代表的な色を抽出するコードを書いてみた。

```Swift:LumberColorSet.swift
    init?(image: UIImage) {
        
        guard let components = image.cgImage?.getPixelColors(count: 6),
              components.count == 6 else {
            return nil
        }
        
        self.init(baseColorComponents: [
                    components[0][0], components[0][1], components[0][2]
                  ],
                  centerBaseColorComponents: [
                    [components[1][0], components[1][1], components[1][2]],
                    [components[2][0], components[2][1], components[2][2]]
                  ],
                  smoothRingColorComponents: [
                    components[3][0], components[3][1], components[3][2]
                  ],
                  roughRingColorComponents: [
                    [components[4][0], components[4][1], components[4][2]],
                    [components[5][0], components[5][1], components[5][2]]
                  ])
    }
```

iOS14から追加された `CIFilter` の `CIKMeans` を使用してイメージの代表色を抽出してみる。k-means法で代表色を抽出するアルゴリズムらしい。

６色必要なので `"inputMeans"` に代表的な6色を与え、 `filter.outputImage` が 6 x 1で得られるような属性にする。
属性は[こちらのURL](https://note.com/yogox/n/n87f614a66d7a)を参考にした。

```Swift:LumberColorSet.swift
    private func colorSampleImage(count: Int) -> CGImage? {
        
        let inputImage = CIImage(cgImage: self)

        guard let filter = CIFilter(name: "CIKMeans") else { return nil }
        filter.setDefaults()
        filter.setValue(inputImage, forKey: kCIInputImageKey)
        filter.setValue(inputImage.extent, forKey: kCIInputExtentKey)
        filter.setValue(64, forKey: "inputCount")
        filter.setValue(10, forKey: "inputPasses")
        let seeds = [CIColor(red: 0, green: 0, blue: 0),    // black
                     CIColor(red: 1, green: 0, blue: 0),    // red
                     CIColor(red: 0, green: 1, blue: 0),    // green
                     CIColor(red: 1, green: 1, blue: 0),    // yellow
                     CIColor(red: 0, green: 0, blue: 1),    // blue
                     CIColor(red: 1, green: 1, blue: 1)]    // white
        filter.setValue(seeds, forKey: "inputMeans")
        guard let outputImage = filter.outputImage else { return nil }

        let context = CIContext(options: nil)
        return context.createCGImage(outputImage, from: CGRect(origin: .zero, size: outputImage.extent.size))
    }
```

上記で取得した6 x 1のイメージから１ピクセルずつRGBを取得する。

```Swift:LumberColorSet.swift
    private func getPixelColors(count: Int) -> [[CGFloat]] {
        
        var components: [[CGFloat]] = []
        guard let importantColorImage = colorSampleImage(count: count) else { return components }
        
        (0...count).forEach { index in
            
            let scale: CGFloat = 1
            let rect = CGRect(x: CGFloat(index) * scale, y: 0, width: scale, height: scale)
            if let cropImage = importantColorImage.cropping(to: rect),
               let color = cropImage.averageColor() {
                
                var r: CGFloat = 0
                var g: CGFloat = 0
                var b: CGFloat = 0
                var a: CGFloat = 0
                color.getRed(&r, green: &g, blue: &b, alpha: &a)
                components.append([r, g, b])
            }
        }
        return components
    }
```

1 x 1のイメージから平均色を抽出する。

```Swift
    private func averageColor() -> UIColor? {

        let inputImage = CIImage(cgImage: self)

        guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: inputImage.extent]) else { return nil }
        guard let outputImage = filter.outputImage else { return nil }

        var bitmap = [UInt8](repeating: 0, count: 4)
        let context = CIContext(options: nil)
        context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

        return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
    }
```

クリスマスツリーから抽出した結果
＊適当に拾ったフリー素材
![xmas_tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/6c9ebff5-3429-953b-26b9-3886ea6d86a7.png)

抽出した結果は以下の色になった。
`#063902` `#241410` `#070707` `#080808` `#050505` `#797979`

参考にしたサイトでも言及されている通り抽出した色はやや意図通りではなくk-means法による？補正があるようなので、以下で彩度と明度を調整してみる。

```Swift
let color = cropImage.averageColor()?.color(mimimumBrightness: 0.5).color(mimimumSaturation: 0.5)
```
```Swift
extension UIColor {
    
    func color(mimimumBrightness: CGFloat) -> UIColor {
        
        var h: CGFloat = 0
        var s: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        
        if b < mimimumBrightness {
            return UIColor(hue: h, saturation: s, brightness: mimimumBrightness, alpha: a)
        }
        return self
    }
    
    func color(mimimumSaturation: CGFloat) -> UIColor {
        
        var h: CGFloat = 0
        var s: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        
        if s < mimimumSaturation {
            return UIColor(hue: h, saturation: mimimumSaturation, brightness: b, alpha: a)
        }
        return self
    }
}
```

明度のみ調整した結果
`#0D8004` `#804739` `#808080` `#808080` `#808080` `#808080`

明度も彩度も調整した結果
`#0D8004` `#804739` `#804040` `#804040` `#804040` `#804040`

うーん、、、あまり期待値は得られなかった。
![figure22-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a7d08011-c93f-2b58-05c1-22dbe8cb860d.png)

明らかに取れそうなイメージで試してみる。
ラスタカラー。
![rasta.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/3991da69-59d4-2ee4-24b5-40181be6bd8c.jpeg)

`#010101` `#560200` `#035500` `#545400` `#000000` `#000000`
明度・彩度を弄った色
`#804040` `#800300` `#058000` `#808000` `#804040` `#804040`

なんとなく近い色は取れたけどやはり意図しない補正がかかってしまう。
![figure22-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/bab5e638-4c90-91dc-d07a-af6c5791cb81.png)

杉や赤松など他の木目イメージから色をサンプリングして適用してみたけど、
あまり良い結果が得られなかったので割愛する。。

# 感想
コードで木目という有機的なものを描こうと思ったことはなかった。
規則性を見出すことでコードに落とし込める発見があるのと、よりリアルに近づけていくにはどういうイレギュラーを加えれば良いのか試行錯誤するのが楽しかった。
木の節はどこかに入れてみたかったが難しくて断念した。