覚書:PCメガネの選び方

PCメガネを選んだ時の観点をまとめる。

観点

  • ブルーライトカット率の計算にどの規格のものを利用しているか。
  • レンズが「コーティング式」か「練り込み式」か

ブルーライトカットの計算規格

以下の3つの規格が利用されることが多い。

  • JIS規格
  • EN規格
  • BS規格

JIS規格とEN規格はほぼ同様のものと思って良いが、BS規格については前者2つに比べて計算結果が大きな値になりやすい。

例えばJIS規格で50%cut、BS規格で50%cut、というように表示されていたとしてもBS規格→JIS規格に計算し直したときに40%cut程度あることがある。

Zoffブルーライトカットレンズの紹介ページを見てみると、ページの最後の注釈に以下の記述がある。

ブルーライトカット率は旧英国規格(BS2724-1987)に基づき算出しています。

www.zoff.co.jp

使い心地というのは使ってみなければわからないものだとは思うが、正直商品を実際のものよりもよく見せたいために数値が大きく出る計算も方法を利用しているのかと思えるので、あまり効能を求めてZoffからPCメガネを買いたいとは思えない。

ただ、安い値段でファッション性の高いフレームが手に入ると思うので、その辺りを気にするならZoffを選択肢に入れるのは良いかもしれない。

「コーティング式」OR「練り込み式」

ブルーライトカット対応のレンズの製法には2種類ある

  • コーティング式
  • 練り込み式

コーティング式は普通のレンズの表面にブルーライトカット用のコーティングを施したもので、練り込み式はレンズ自体の素材にブルーライトをカットする素材を利用したもの。

コーティング式のものは安価であるというのが長所であるが、長年利用するとコーティングが剥がれてカット効果がなくなってしまったり、光源のライトなどが反射しやすく集中しづらいなどの短所がある。

練り込み式のものは逆で、カット効果がなくなってしまうこともなく、反射もしづらい。ただコーティング式のものよりかは高い。

使いやすさの観点もあると思うので、練り込み式の方が良いように思える。ただ、値段と相談したいとも思うので、安価なコーティング式のものを探すのもやむなしかなと思う。

MTLViewの描画の流れについてのドキュメント

これが主にMetalKitのMTLViewのセットアップについて書かれているドキュメント developer.apple.com

これが主にMetalKitのMTLViewでの描画について説明しているドキュメント developer.apple.com

MTLViewで表示を行うときの流れとしては以下の通り。先週書いたgpuで簡単な配列計算をした時とそこまで変わらないみたい。

  1. MTLViewインスタンスのdeviceプロパティに使いたいGPUのMTLDeviceインスタンスを割り当てる
  2. RenderPipelineを作成する。この時MTLRenderPipelineDescriptorを利用する。本来のMTLRenderPipelineDescriptorインスタンスからPipelineの一部を変更して、自分の描画したいものを描けるようなPipelineにする
  3. MTLDeviceインスタンスからMTLCommandQueueを作成する
  4. MTLView Delegateを作成し、MTLViewインスタンスに設定する
  5. MTLView Delegateのdrawメソッド内でCommandBufferの設定し、実行後に描画を行う
    1. 先に作成したPipelineを使用するCommandBufferを作成する。
    2. CommandEncoderを作成してPipelineのインプットし最後にviewのMTLViewのdrawableプロパティを作成したテクスチャで置き換える処理をPassに追加する
    3. 実行

RenderPipelineはいくつかのステップに分かれているので、そのステップのうちいくつかを自前のものに置き換えてやることで自分の描きたいテクスチャを表示することができる。今回ドキュメントでは、図形の描画をするため図形の頂点情報を出力する段階であるVertex functionと図形内部のピクセル情報を変更できる段階のFragment functionの変更を行なっていた。

感想

先週は体調悪くてコード書いて見るところまで行かんかったので、今週はMSLとか自分で書いてみたい。

Performing Calculations on a GPUを読んだ

将来的にMetalでiOSのAppのViewを表示したりしたいなあと思ったので、第一歩として以下のAppleのページを見ながらMetalを扱うためのコードを書いてみた。 developer.apple.com

以下は2つのIntのリストのそれぞれの要素を足し合わせる計算をGPUにやらせるコードである。

GitHub - rikinyan/MetalCalcSample

import Foundation
import Metal

guard let gpuDevice = MTLCreateSystemDefaultDevice() else {
    throw NSError(domain: "device", code: 1)
}

let intByte = Int.bitWidth / 8
let arrayLength = 10
let bufferLenght = intByte * arrayLength
var bufferA = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)
var bufferB = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)
var bufferResult = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)

for index in 0..<arrayLength {
    let byteOffset = index * intByte
    bufferA?.contents().storeBytes(of: Int.random(in: 1...10), toByteOffset: byteOffset, as: Int.self)
    bufferB?.contents().storeBytes(of: Int.random(in: 1...10), toByteOffset: byteOffset, as: Int.self)
}

guard let shaderLibrary = gpuDevice.makeDefaultLibrary() else {
    throw NSError(domain: "lib", code: 1)
}

guard let addFunc = shaderLibrary.makeFunction(name: "add_arrays") else {
    throw NSError(domain: "func", code: 1)
}

guard let pipline = try? gpuDevice.makeComputePipelineState(function: addFunc) else {
    throw NSError(domain: "pipline", code: 1)
}

guard let commandQueue = gpuDevice.makeCommandQueue() else {
    throw NSError(domain: "queue", code: 1)
}

guard let commandBuffer = commandQueue.makeCommandBuffer() else {
    throw NSError(domain: "buffer", code: 1)
}

guard let commandEncoder = commandBuffer.makeComputeCommandEncoder() else {
    throw NSError(domain: "encoder", code: 1)
}

commandEncoder.setComputePipelineState(pipline)
commandEncoder.setBuffer(bufferA, offset: 0, index: 0)
commandEncoder.setBuffer(bufferB, offset: 0, index: 1)
commandEncoder.setBuffer(bufferResult, offset: 0, index: 2)

let threadPerGrid = MTLSize(width: arrayLength, height: 1, depth: 1)
var threadGroupSize = pipline.maxTotalThreadsPerThreadgroup

if threadGroupSize > arrayLength {
    threadGroupSize = arrayLength
}

commandEncoder.dispatchThreadgroups(
    threadPerGrid,
    threadsPerThreadgroup: MTLSize(width: threadGroupSize, height: 1, depth: 1)
)

commandEncoder.endEncoding()

commandBuffer.commit()

commandBuffer.waitUntilCompleted()

for index in 0..<arrayLength {
    let byteOffset = index * intByte
    print(
        bufferResult?.contents().load(fromByteOffset: byteOffset, as: Int.self)
    )
}

MSL:metalを介してGPUで実行されるコード

#include <metal_stdlib>
using namespace metal;

kernel void add_arrays(device const int* inA,
                       device const int* inB,
                       device int* result,
                       uint index [[thread_position_in_grid]])
{
    result[index] = inA[index] + inB[index];
}

全体の流れ

  1. 今デバイスが持っているGPUを扱うための、GPUを抽象化したオブジェクトを作成する。
  2. 計算に利用するデータを確保する。
  3. Appをビルド時に、自作のMSLの関数を持つライブラリが自動でAppに埋め込まれるので、その関数を参照するオブジェクトを作る
  4. 関数を利用するためのパイプラインを作る。
  5. GPUに命令を送るためのキューを作成する
  6. キューに入れる命令を作成する
  7. 命令で利用するスレッドの割り当て設定
  8. 実行

詳細

まず、MTLDeviceというオブジェクトを作成する。GPUを抽象的に表すものであり、GPUに関するさまざまなオブジェクトはMTLDeviceから作成することになる。

GPUで利用するデータはMTLResourceといい、今回はこれを継承したMTLBufferを利用し、事前に計算に必要なデータをセットしたListと計算結果を格納するListを確保した。Bufferを作成するときにオプションとして.storageModeSharedを設定しているが、これはCPU、GPUの両方からアクセスできるメモリとして領域を確保する設定をしている。

次にMSLで書かれた、GPUで実行する命令を参照するオブジェクトを作る。MSL(Metal shading language)というのがGPUで実行するためのコードを記述するためのC++からの派生の言語になる。プロジェクトをビルドするときにそのプロジェクトに含まれているMSLのコードも自動で埋め込まれ、MTLLibraryからMTLFunctionというオブジェクトで関数を参照することができる。また、GPUで実行されるコードのことを慣例的にシェーダと呼ぶらしい。

パイプラインというのが実際に実行されるコードになる。MTLFunctionはあくまでMSLで書かれたコードへの参照であるため、実行できる形式にコンパイルする必要がある。パイプライン作成時にGPUで実行される状態へMSLがコンパイルされる。

この作成したパイプラインをGPUに送るためにMTLCommandQueueというキューを作成する。また、このキューから送信する命令に関するデータのまとまりであるMTLCommandBufferを作成する。MTLCommandEncoderを用いてMTLCommandBufferに実行するパイプライン、パイプラインで利用する引数などの情報を設定する。

最後にMTLCommandEncoderで命令が実行されるスレッドの設定をする。MSLのコードをみて、Listの足し合わせをするのにループが存在しないが、これは各要素ごとの計算を複数スレッドで実行するためである。そのため実行するスレッド数は配列の要素と同じ数になる。threadgroupはメモリブロックを共有するスレッドの集まりであり、今回は全て同じグループとして扱っている。

ここまできたらMTLCommandEncoderを終了し、MTLCommandBufferをコミットして実行する。

CALayerでグラデーションのアニメーションを作成する。

以下のようなものを作成する。普段CALayerを利用して何かやる機会が少ないので、ちょっと久しぶりにいじってみたくなった次第。

UIKit gradetion animation · GitHub

UIViewはlayerというCALayerのプロパティを持っているが、これを派生タイプであるCAGradientLayerに置き換えたい。ただ、layerプロパティはget onlyであり、代入することができないため、今回はsublayerとしてCAGradientLayerを追加する。

//
//  GradientView.swift
//  GradientSample
//

import UIKit

class GradientView: UIView {
    var gradientLayer: CAGradientLayer! = nil
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    override class func awakeFromNib() {
        super.awakeFromNib()
    }
    
    private func setup() {
        gradientLayer = CAGradientLayer()
        
        gradientLayer.frame = CGRect(
            x: 0,
            y: 0,
            width: layer.bounds.width,
            height: layer.bounds.height
        )
        
        gradientLayer.colors = [
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
        ]
                
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
        layer.addSublayer(gradientLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = CGRect(x: 0, y: 0, width: layer.bounds.width, height: layer.bounds.height)
    }
}

以下のようになる。ちなみにCAGradiationLayerのstartPointプロパティendPointプロパティとでグラデーションの方向が決まる。

これをループするアニメーションにしていく。 とりあえず「1周期分のアニメーションを作成して、それを無限ループさせる」という考えてやっていく。 ここでの「1周期分のアニメーション」は、「左上のオレンジの部分が右下を通り過ぎ、再度左上に出てくるまで」ということである。

CALayerをアニメーションさせるにはCAAnimationを利用する。CAAnimation自体のインスタンスを生成は普通はしないため、今回はその派生タイプであるCABasicAnimationというのを利用し、アニメーションを作成する。

それぞれの色のグラデーションの終了点を表すlocationsプロパティでどこまでグラデーションをかけるのかが決まり、このプロパティをCAAnimationで徐々に変動させることでアニメーションにする。

、今回の場合はグラデーションに使う色を「橙、青、橙、青」を1列に用意しておき、その4つのlocationを短調増加させるだけで1ループ分のアニメーションとした。

わかりづらくて申し訳ないとは思う

これを実現するためにsetup関数の中身を変える。

    private func setup() {
        gradientLayer = CAGradientLayer()
        
        gradientLayer.frame = CGRect(
            x: 0,
            y: 0,
            width: layer.bounds.width,
            height: layer.bounds.height
        )
        
        gradientLayer.colors = [
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
        ]
        
        gradientLayer.locations = [
            -2,
            -1,
            0,
            1,
        ]
        
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
        layer.addSublayer(gradientLayer)
        
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [-2, -1, 0, 1]
        animation.toValue = [0, 1, 2, 3]
        animation.duration = 5
        gradientLayer.add(animation, forKey: nil)
    }

これで1ループ分のアニメーションができた。

あとはこれを無限ループさせるようにする。 CABasicAnimationにループ回数を指定するためのプロパティがあるので、これを無限回に指定すれば良い。

……setupの中身
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [-2, -1, 0, 1]
        animation.toValue = [0, 1, 2, 3]
        animation.duration = 5
        animation.repeatCount = .infinity // これ
        gradientLayer.add(animation, forKey: nil)
      }

感想

subLayerにCAGradientLayerを追加したが、そもそもUIViewのlayerClassを変更すれば良かったのではと思った。まああとの祭りやね。

Core Animation Basicsを読んだ。

運が良いことにこれまで複雑なViewのアニメーションやらの実装に遭遇することなく今まで仕事をして来れたので、「そういえばCALayer関連の基礎知識が抜けているなあ」と思ったので以下のDocumentを読んで、知らなかったことを書いていく。

developer.apple.com

Core AnimationはViewのコンテンツをbitmapに変換している

UIKitでは、描画するコンテンツとアニメーションの情報はCore AnimationというFrameworkで管理・操作している。 このFrameworkの内部で「Viewに表示するContentをbitmapとして保持して、指定の操作をbitmapに施し、実際にViewに表示する」ということをしている。

この時、bitmapについての情報を保持しているオブジェクトがCALayerであり、ユーザはこのCALayerに対して命令を出したりすることになる。(実際にはUIViewがCALayerのラッパーの役割をしており、簡単な操作なら直接CALayerを操作する必要はない)

このCALayerを介してbitmapを操作すると、Core Animationはグラフィック専用のHardwareを使って処理を行うらしく、たとえばViewを何十回も生成し直してアニメーションを作ったりするよりも、CALayerを介して「アニメーション時にパラメタの最終値と変化の流れ」を指定してあげる手法でアニメーションを作成する方が効率が良い。

CALayerは実際は3次元空間を想定している

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Art/transform_basic_math_2x.png

座標操作時、CALayerでは以上のような行列式で(x, y, z)の値を変化させている。普段意識していなかったが、画像のようにz座標に対しての変換もCALayerでは想定している。

目的別の3つのLayer setがある

Core Animationで3つのLayer setを保持している。

  • model layer tree
    • アニメーションの終着点を保持しているLayer。ユーザが「このようなアニメーションをしたい」と思った時、実際アニメーションに利用するパラメタの最終値を設定することになるが、その値を保持しているLayer set。
  • presentation tree
    • アニメーション途中におけるパラメタを持っているLayer set。(一応このアニメーション途中のパラメタは修正できるようだが、するべきでないと書かれている)
  • render tree
    • 実際にアニメーションが行われているLayer set。(このLayer setはCore Animation内でPrivateになっておりユーザから値を修正することはできない)

なぜ3層に分かれているのか、という記述が特になかったが、単純に「ユーザがアニメーションの設定をする層」「アニメーション中のパラメタを参照出来る層」「Core Animationで実際に操作し、ユーザに直にいじらせたくない層」で役割分担をした結果ではと思う。

感想

思えば、UIViewの「Viewのカドを丸くしたい」みたいな時にしかCALayerを直接いじるみたいなことはしてこなかった気がする。

また、CALayerが3次元空間を想定しているみたいなので、Paypayアプリのカードのフリップアニメーションも簡単に実装できるのかもなと思ったので、こちらはちょっとやってみたい。

ScrollTargetBehaviorとScrollTarget

正直自分のやりたいことにそぐう機能ではなかったが、せっかく読んだのでメモしておこうと思った次第。

developer.apple.com developer.apple.com

ScrollViewはスクロールして指を離したときのスクロール速度などから最終的に終着点がどこなのか、というの計算しているらしく、その終着点を変更するのにScrollTargetBehaviorを利用する。

ScrollTargetはその終着点がどこなのか、を表す。

以下の例ではscrollTargetBehaviorViewAlignedScrollTargetBehaviorというScrollTargetBehaviorを登録している。これをしておくと、ScrollTargetがどこかのScrollView内のItemになり、そのItemがViewの左端にピッタリくっつくようになる。

(注意) ViewAlignedScrollTargetBehaviorを利用するためにはItemたちのContainerとなるViewに.scrollTargetLayout())をつけなければならないことに注意すること。

ScrollTargetBehavior & ScrollTarget · GitHub

import SwiftUI

struct SampleScrollView: View {
    private(set) var numberContainers: [NumberContainer] = []

    
    init(numbers: [Int]) {
        numberContainers = numbers.map { value in
            NumberContainer(id: value, value:  value)
        }
    }
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 0) {
                ForEach(numberContainers) { container in
                    NumberPanel(number: container.value)
                }
            }
            .scrollTargetLayout()
        }
        .frame(width: 400)
        .scrollTargetBehavior(.viewAligned)
        .border(.gray)
        .padding(10)
    }
}

感想

自分としては、「中央に一番近いItemをScrollViewの真ん中に持ってきたい」というのがやりたいことだったのだが、自分ではちょっと難しいなと感じた。必要なこととして、

  • 1つ1つのItemの、ScrollView内での位置を取得する。
  • ScrollView内の中央の位置を取得する。
  • Itemの位置とScrollViewの中央の位置を比較して、移動させるに相応しいItemを決定する。
  • 実際に真ん中に移動させる

という操作が必要になると思うが、どれも自分の探した限りではできそうになかった。もうちょっと使いやすくならんかしらね。

awaitはあくまでsuspentionが起こる「かもしれない」場所

同期関数は非同期関数とも取れる

SwiftのAsync/Awaitのproposalを読んでいて「へぇ〜」って思ったことのメモ書き。

要するにこのようなこと。

var asyncFunction: (() async -> Int)
let syncFunction: (() -> Int) = {
    return 1 + 1
}

asyncFunction = syncFunction

Task {
    print(await asyncFunction())
}

以上のようにasync関数の入れ物には普通の同期的な関数を入れることができる。ただもちろん実行するときにはawaitをつけなければならない。

他にも以下のようにprotocolの中で用意されているasync関数の入れ物にも、継承先で同期関数を代わりに入れることができる。

protocol AsyncProtocol {
    func calc() async -> Int
}

class CalcClass: AsyncProtocol {
    func calc() -> Int {
        1 + 1
    }
}

これを見ると「protocolを複数のクラスに適応したいが、クラスAの処理を非同期、クラスBの処理を同期にしたい」みたいな場合に便利かもしれない。

これができなくともasync関数の中身に実際に非同期な処理を書かずに同期的な処理を書けば良いかもしれないが、処理内容を見る前に関数宣言時点で同期的か非同期的かがわかりやすくてこっちにも利点があるかなと思う。

awaitについて

proposalを読んでいると、どうにもawaitがある箇所というのはそもそも「potential suspension point」というように呼ばれていて、potentialとあるように「suspentionが起こるかもしれない場所」であるらしい。

なので、awaitの地点で実行されるasync関数の中身が実際に非同期なのか同期なのかどうかというのは問題にならない。

実際にasyncとついていない同期関数の前にもawaitをつけて実行することができる(ただし「ここ非同期じゃないよ」とwarningが出る)。というか先に出した通りasyncを関数につけたとしてもその中身が非同期である必要もないわけだしそりゃそうかも。

また、async/awaitというのはthrows/tryの使われ方を参考にして実装されているみたいで、async/awaitと同様に、「throwsなし関数はthrowsあり関数とも取れる」みたい。

var throwFunction: (() throws -> Int)
let noThrowFunction: (() -> Int) = {
    return 1 + 1
}
throwFunction = noThrowFunction

try? throwFunction()

protocol ThrowProtocol {
    func calc() throws -> Int
}

class CalcClass2: ThrowProtocol {
    func calc() -> Int {
        1 + 1
    }
}

実際使う?

分からん……ただまあコードをわかりやすく書く、みたいな点で言えば使うかもしれないし、ちょっと面白いなと思ったので覚えておきたい。