キーキャップ設計による OpenSCAD 入門

自作キーボード Advent Calendar ６日目の記事です。

昨日の記事は、sirojake くんの「[マサコの知らない自作キーボードの世界　クリスマスSP](https://qiita.com/sirojake/items/2e1c45f346b9ba3b9e7f)」でした。

この記事では、フルスクラッチで簡単なキーキャップのモデルを書きながら、 OpenSCAD というオープンソース 3D CAD の使い方を紹介します。

また、設計したモデル（特にキーキャップ）を DMM.make さんなどの 3D プリント業者に発注する際のノウハウなどにも触れます。

※この記事で設計するキーキャップモデルは、自作する際の叩き台のようなものを想定しています。最低限、実際に出力できるレベルのものにはなっているのではと思っていますが、時間の都合試せていないので、自己責任にてお願いいたします。

## コラム：私がキーキャップを 3D プリントするまで

私の場合、自分に完全にしっくりくるキーキャッププロファイルがなかったのがきっかけで、キーキャップの設計をはじめました。

[MDA プロファイル](https://kbdfans.cn/collections/mix-profile/products/big-bang-mda-profile-ortholinear-keycaps) の「低め・広め・Spherical Top」はいいなあと思いつつ、とはいえ [SA プロファイル](https://pimpmykeyboard.com/key-cap-family-specs/) のように R3 は平坦でいて欲しいなあとか、逆に傾斜を SA に寄せるなら R2, R4 と同じように 1 列目・ 6 列目も内向きに傾いていいのでは？とか。いろいろ考えているうちに、これは自分で作ってみたほうが早いなとなりました。

私が最初に手をつけたのは [Corne Keyboard](https://booth.pm/ja/items/1107432)　の親指用キーキャップでした。

![corne.jpg](https://qiita-image-store.s3.amazonaws.com/0/54240/a8fe45b6-c543-692a-e784-100225b877ea.jpeg)

Corne は 30% キーボードに親指キーを生やしたようなレイアウトなのですが、タイプするときの姿勢を考えると、親指キーだけは高さが低めでもいいのではと感じていました。ちょうどいい 1.5u キーが手に入っていなかったのもあったので、まずは親指キーを自作することから始めました。

OpenSCAD は触ったことがありましたが、キーキャップのモデルは作ったことがなかったので、 [KeyV2](https://www.thingiverse.com/thing:2783650) というキーキャップモデルをカスタマイズすることから始めました (天キーというキーボードイベントでもんくさんも紹介してました)。この記事で作るキーキャップの実装も、このライブラリの影響を強く受けています。

親指キーキャップは、記憶が確かなら 5 回目くらいの試作で一旦完成したと思います。 9 キーセット (両手分 + 予備) を一度試作するのに、一番安い素材でも 1000 円程度、実用に耐える素材だと 2000 円以上かかるので、やっぱりどうしてもそれなりの予算が必要になります。また出力を業者に依頼する場合、モデルを書いてから実物が届くまでにどうしても一週間程度はオーバーヘッドが出てしまうので、なかなかトライアンドエラーのスピードが出せないのもつらかったです。

ただ結果はとてもいい感じで、「MX スイッチにはまる限界の低さの 1.5u かつ、縦方向の Cylindrical Top」という、おそらく普通に探しても手に入らない、自分のニーズにドンピシャのキーキャップができました。このキーキャップは [こちら](https://make.dmm.com/item/977532/) で実際に出力することもできます。

![3d_keycap.png](https://qiita-image-store.s3.amazonaws.com/0/54240/d200967a-1c04-94a4-4824-49865dbe7317.png)

今は、「行ごとだけじゃなく、全てのキーの形状がそれぞれ違ってもいいのでは？」という発想から、既存キーボードにはめるだけで 3D キーボードになるような、特殊プロファイルキーキャップの試作をしています。ちょうどああでもないこうでもないと試行錯誤しているタイミングで、 Colosseum や Lime など国内の 3D プリントキーボードの開発もにわかに盛り上がってきたので、流れてくる写真とにらめっこしながら、なるほどーと唸ったりしています (カーブの設計が人それぞれで、とても難しい＆面白い)。

フルのキーセットともなると一度の試作に 9000 円近くかかってくるのですが、乗りかかった船、やるからには End Game なキーキャップを作りたいなと思っています。まもなく第二、三弾の試作を発注する予定で、良いものに仕上がったら販売もできればと思っているので、ぜひよろしくおねがいします！この記事では紹介しきれなかった工夫も含め、たくさんのパターンを出し、ああでもないこうでもないと考えながら、いくつかは実際に試作もして、詰めているので、きっといいものができると信じています（きっと…）。

ここからはいよいよ記事の本題です。

## OpenSCAD の特徴

OpenSCAD は一般的な 3D CAD と異なり、プログラミング言語のような格好をしています。

したがって、

- 幾何学的な図形が得意（ループで似たような図形を大量生産する、とかできる）

  - 逆にフィギュアのような手の込んだ造形は苦手

- プレーンテキストなので *git 管理できる*

などの特徴があります。

特に git 管理できるのはかなり便利で、ブランチをバンバン切っていろんなバージョンを並行して試作することができたり、差分もプレーンテキストで簡単に確認できたりします (レンダリング結果を目 diff するのはつらいですよね)。

## インストール・使い方

ビルド済みのバイナリが [公式](http://www.openscad.org/) で配布されているので、これを使うのが簡単です。

<img width="899" alt="ide.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/383a2807-49b0-a7f3-84ef-a4525f8c46b6.png">

起動するとエディタとプレビューのペインがセットになった IDE のようなものが立ち上がるので、基本的にはこの画面で開発を進めます。

一応、 CUI から使える `openscad` コマンドもあるので、お好みのエディタでコードを書きながら、このコマンドでビルド・プレビューすることもできます。

(拙作の Emacs プラグイン [scad-preview.el](https://github.com/zk-phi/scad-preview) は中でこのコマンドを叩いています)

## ステムを作る

<img width="407" alt="keycap_stem.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/1133366c-618f-87e1-c7dd-6d9f811aa876.png">

まずはキーキャップの根幹部分、ステムを作りながら、 OpenSCAD の基本的な構文を見ていきます。

### 外形を作る / プリミティブ (cylinder)

OpenSCAD のコマンドには大きく分けて二種類のものがあります：

- 基本的な図形を作るもの

- ある図形を変形させて、別の図形にするもの

「基本的な図形を作るもの」としては、

- 直方体 `cube`
- 球 `sphere`
- 円筒 `cylinder`

と、座標指定で任意の多面体を直接作る `polyhedron` があります。

ここではこのうち、円筒を作る `cylinder` を使って、ステムの外形を作ってみます。

ここで注意しなければいけないのは、

<img width="231" alt="mx_data_stem.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/b0999f1b-583f-9afc-1a85-5aec5e1da5ac.png">

スイッチのいわゆる「軸」部分より太く作ってしまうと、そこが引っかかってスイッチを押せなくなってしまうことです。一方で小さく作りすぎると、 3D プリンタの精度や材質によっては破損してしまうため、適宜チューニングする必要が出てきます (データシートの画像は Cherry 公式から引用)。

ここではとりあえず KeyV2 のデフォルト値に採用されていた `5.5mm` を採用することにします。高さは仮に、十分な高さを設定します。

<img width="413" alt="openscad_cylinder.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/42f25638-fded-9acb-8a28-f8f703ff5a5e.png">

``` openscad
cylinder(d = 5.5, h = 15);
```

レンダリング結果を見るとわかるように、曲線は短いたくさんの直線で近似されます。近似精度は `$fs` という組み込み変数で調整でき、 3D プリンタの精度を考えると `0.1mm` くらいあれば十分そうです。

<img width="411" alt="openscad_fs.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/f6d6c5bf-f56f-0656-67bb-4f9439913a38.png">

``` openscad
$fs = 0.1;
cylinder(d = 5.5, h = 15);
```

ステムの太さは、あとでチューニングすることを考えると、変数としてくくり出しておいても良いかもしれません。 OpenSCAD で
は変数宣言に特別な構文は必要ありません。

``` openscad
$fs = 0.1;
stem_outer_size = 5.5;
cylinder(d = stem_outer_size, h = 15);
```

### ステムに穴を空ける / difference, translate

ステムの外形ができたら、次はキースイッチの十字部分をはめるための穴を開けていきます。

既存のオブジェクトに穴を空けるには、 `difference` コマンドが便利です。このコマンドは n 個のオブジェクトを受け取って、先頭から残りのものを引き算します。

例えば、先ほど作ったステムの外形から、一回り小さい円筒を引き算すると次のようになります：

<img width="406" alt="openscad_difference.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/b4f56c50-9234-2414-43cb-fa8617ad948f.png">

``` openscad
$fs = 0.1;
stem_outer_size = 5.5;

difference () {
  cylinder(d = stem_outer_size, h = 15);
  cylinder(d = stem_outer_size / 2, h = 15);
}
```

同じ要領で、キースイッチの十字を先ほどのステムの外形から引き算してやれば、スイッチのぴったりはまるステムができあがります。

ここでステムの十字の太さが問題になってくるのですが、実は十字の太さはスイッチによって微妙に違います。 Kailh BOX 事件 (詳しくはググってください) の頃に、メジャーなスイッチの十字の太さを計測した [記事](https://www.reddit.com/r/MechanicalKeyboards/comments/91u9d4/kailh_box_switch_stem_measurements_and_possible/) が上がっていたりするので、このあたりも参考に、それらしい値を設定すると良さそうです。なお、データを見るとわかる通り、十字の横軸と縦軸では微妙に太さが違い、したがって実はキーキャップには向きがあるので、注意してください (あえて、多少きつく or ゆるくなってしまうのを覚悟で縦横同じ太さにして、回転しても使えるようにするというのも一つの手です)。

経験上、スイッチ側の十字とぴったりサイズくらいから試作を始めて、適宜きつめていくのが良さそうです。ぴったりサイズだとたいていゆるめに仕上がるので、「ちゃんとはめられるけど、簡単にポロポロ取れたりはしない」くらいのバランスになるように少しづつきつくしていきます。いきなりきつめのものを作ってしまうと、そもそもスイッチにまったく挿入できないので難儀します。一方、多少ゆるいくらいなら、気にはなってもまったく使えないということはないですし、あるいは下駄を履かせてフィットさせるなど、手立てがあります (ラップを詰めると良い感じになるという話があります)。また、素材によっても選ぶべき寸法は変わってくるようです。というのも、柔らかめの素材であれば、ジャストサイズより少しきつめに作って、グッと軽く力を入れて挿入するくらいに調整した方が、よりがっちりはまって安定します。一方、そのまま同じモデルを金属など硬い素材で作ると、今度はきつすぎて入らなくなってしまう場合があるようです。このように、硬度の大きく違う素材を使う場合は別々にチューニングする必要がありそうなので、特に販売など計画されている方は注意してください。

さて、寸法が決まったら、実際に十字を作っていきます。今回は上に挙げた記事の計測値ほぼそのままで作ってみることにします (おそらくゆるめのものができあがると思います)。

十字は直方体 (`cube`)　二つを組み合わせれば作れそうです。高さは上で作ったステムの外形に合わせておくことにします。

<img width="412" alt="openscad_bad_cross.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/eb7315c9-08d7-734a-3ba0-59654ffc3fdd.png">

``` openscad
$fs = 0.1;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

difference () {
  cylinder(d = stem_outer_size, h = 15);
  // 十字部分
  cube([stem_cross_h, stem_cross_length, 15]);
  cube([stem_cross_length, stem_cross_v, 15]);
}
```

おっと、ちょっと期待と違う図形になってしまいました。実は、 `cylinder` コマンドでは底面の円の中心が空間の原点に来るのに対して、 `cube` コマンドでは直方体の頂点が空間の原点にきます。そこで、物体を移動するコマンド `translate` を使って、期待する位置に移動してあげます。

<img width="408" alt="openscad_cross.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/a77f0b9a-8265-b3c0-6bf1-e270f96ae76f.png">

```openscad
$fs = 0.1;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

difference () {
  cylinder(d = stem_outer_size, h = 15);
  // 十字部分
  translate([- stem_cross_h / 2, - stem_cross_length / 2, 0])
    cube([stem_cross_h, stem_cross_length, 15]);
  translate([- stem_cross_length / 2, - stem_cross_v / 2, 0])
    cube([stem_cross_length, stem_cross_v, 15]);
}
```

今度はそれらしい立体ができました。

#### 別解：cube の center オプション

実は `cube` には `center` というオプションがあって、これが `true` の場合、直方体の中心が空間の原点になります。

これを利用すると、上のモデルは次のようにも書けます：

```openscad
$fs = 0.1;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

difference () {
  cylinder(d = stem_outer_size, h = 15);
  // 十字部分
  translate([0, 0, 15 / 2])
    cube([stem_cross_h, stem_cross_length, 15], center = true);
  translate([0, 0, 15 / 2])
    cube([stem_cross_length, stem_cross_v, 15], center = true);
}
```

`z` 方向の `translate` だけはやはり必要なことに注意してください。

#### おまけ：ステムの外形を cube で作る

家庭の 3D プリンタで出力する場合など、精度が出ない・破損の恐れがある場合には、ステムの外形も `cube` で作ってしまう手もあるかもしれません。例によって、向きには注意してください。

<img width="411" alt="openscad_cubic_cross.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/c71bc775-9861-fe04-71b9-98f50b5266cd.png">

``` openscad
stem_outer_width  = 7.2;
stem_outer_height = 5.5;
translate([0, 0, 15 / 2])
  cube([stem_outer_width, stem_outer_height, 15], center = true);
```

## 外形を作る

ステムが無事完成したので、こんどはキーキャップの外形を作っていくことにします。

<img width="410" alt="openscad_rough_shape.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/f55b0ce8-a918-8d77-d21c-0e5fc72acd15.png">

キーキャップの外形は、トップの凹みなどのディティールをいったん考えないことにすると、おおざっぱに言って「底面と、それより少し小さな上面を結んだ、台形のような形」をしています。

このような図形を作るには、例えば次のような方法が考えられます：

1. 直接座標を指定して `polyhedron` に投げる
2. 底面と上面を作って、その「凸包」を取る
3. 四角錐を作って、それをスライスする

この記事では、まずは 2. の方法でキーキャップの外形を作る方法を紹介します (1, 3 の方法は余裕があったらあとで書きます)。というのも、この方法が一番柔軟に形を作れるように感じるからです。

### ざっくりした形を作る / 凸包 (hull)

いくつかのオブジェクトに対して、それらをまるごと包み込む、一番小さな、凸な物体を「凸包」といいます。

まずは二次元で考えるとわかりやすいかもしれません。

<img width="552" alt="openscad_hull.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/96445d7c-a5f4-6b2a-de5a-f5ba442e0748.png">

黒で示されたオブジェクトたちが与えられた時、それらの凸包は、青のラインで囲まれたような図形です。たとえば、大きな輪ゴムを買ってきて、全ての黒いオブジェクトを含むようにびよーんと伸ばしてかけてから、手を離した時にピタッとフィットする図形が凸包です。

話を CAD に戻します。 OpenSCAD には、３次元のオブジェクトの凸包を取るコマンド `hull` が用意されています。

これを利用すると、キーキャップのおおざっぱな形を簡単に作ることができます：

まず、十分薄い (3D プリンタで再現できない程度) 底面・上面を `cube` で作り、

<img width="411" alt="openscad_slices.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/8038452d-a300-1ee6-df20-00681f4171aa.png">

```openscad
key_bottom_size = 18;
key_top_size    = 14;
key_top_height  = 8;

translate([0, 0, key_top_height])
  cube([key_top_size, key_top_size, 0.01], center = true);
cube([key_bottom_size, key_bottom_size, 0.01], center = true);
```

`hull` でこれらの凸包を取ります。

<img width="404" alt="openscad_shape.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/7dcfad7c-6da0-ebc2-4442-265842f3bb45.png">

```openscad
key_bottom_size = 18;
key_top_size    = 14;
key_top_height  = 8;

hull () {
  translate([0, 0, key_top_height])
    cube([key_top_size, key_top_size, 0.01], center = true);
  cube([key_bottom_size, key_bottom_size, 0.01], center = true);
}
```

底面の大きさですが、一般的なキーボードのキーピッチが 19mm (19.05mm) なので、ここから逆算して考えるのが良さそうです。たとえば 18mm にすれば、キーキャップとキーキャップの間が 1mm 程度開くことになります。あまりギリギリだと隣と干渉してしまう可能性がありますが、あまりにスカスカだとちょっとかっこ悪い感じがします。サンプルコードで採用している 18mm は、やや安全側（スカスカ寄り）の調整になると思います。

上面の大きさも同じく、ある程度好みで調整するポイントになります。市販されているキーキャップでも、たとえば XDA プロファイルや MDA プロファイルなどはトップが広めになっています。一般に、トップが広いほどスカりにくく気持ちよく打てる一方、広すぎると誤爆が増える傾向があると言われます。

<img width="408" alt="openscad_ataru.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/4be0c572-c737-259d-7c5e-03e47d2ae1ae.png">

また、たとえば手前の行と奥の行で段差がないようなキーキャップでは、トップが広すぎると奥のキーを押した時に手前のキーの角が指に当たったりすることもあるかもしれません。実際に試作しないとなかなかわからない部分ではありますが、試作の回数を減らすために、このあたりも意識して設計するといいと思います。

キーの高さも同じくお好みポイントです。パームレストを使用せずに打つ場合にはやや低めにするのが好まれることが多いですが、背の高いキーキャップもレトロ感のある見た目や、重厚感のあるコトコトとした押しごこちになりやすく、根強い人気があります。

### 中身をくり抜く / module

次は、上で作ったキーキャップ外形の中身をくり抜いて、中にステムなどを入れる空間を作ります。

既存の図形に穴を開けるには `difference` が使えることを紹介しました。ここでも `difference` を使って、「キーキャップ外形から、それより一回り小さいキーキャップ外形を取り去る」ことで穴を開けることにします。

似たような図形を複数作るには、 `module` を使うと便利です。 `module` はちょうど、一般的なプログラミング言語の「関数」に似ています。

```openscad
key_bottom_size = 18;
key_top_size    = 14;
key_top_height  = 8;

module keycap_outer_shape () {
  hull () {
    translate([0, 0, key_top_height])
      cube([key_top_size, key_top_size, 0.01], center = true);
    cube([key_bottom_size, key_bottom_size, 0.01], center = true);
  }
}

keycap_outer_shape();
```

`module` は引数を受け取ることもできます。ここでは、キーキャップのサイズを引数で受け取って、一回り小さいキーキャップ外形が簡単に作れるようにしておくと便利そうです。

```openscad
module keycap_outer_shape (key_bottom_size, key_top_size, key_top_height) {
  hull () {
    translate([0, 0, key_top_height])
     cube([key_top_size, key_top_size, 0.01], center = true);
    cube([key_bottom_size, key_bottom_size, 0.01], center = true);
  }
}

keycap_outer_shape(18, 14, 8);
```

自由にサイズを設定できるキーキャップ外形モジュールができたら、満を持してこれを `difference` に投げます：

<img width="411" alt="openscad_digged_shape.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/08972c3d-803c-b9d8-5729-e1bd4154e90e.png">

```openscad
module keycap_outer_shape (...) { ... }

difference () {
  keycap_outer_shape(18, 14, 8);
  // 一回り小さいキーキャップ外形
  keycap_outer_shape(15, 11, 6.5);
}
```

今回は幅が 3mm 小さく、高さが 1.5mm 低いキーキャップ外形をくり抜いてみました。このとき、このとき、くり抜かれたあとのキーキャップは、横の壁も、天井も、 1.5mm 厚になります。この程度の厚みがあれば十分 3D プリントに耐えると思いますが、破損などがあれば適宜調整してください。

スイッチとの隙間には要注意です。くり抜く形がスイッチ自体の形より小さいと、スイッチを最後まで押し込むことができません。穴の最も狭い部分の幅は、上面の幅からさらに壁の厚みを引いたものになります。壁の厚みを考慮し忘れるとスイッチに当たってしまうかもしれないので、ギリギリの設計をする場合はきちんと考慮に入れてつくるようにしましょう。特に上面の狭めなキーキャップや、背の低めなキーキャップを作りたい場合、引っかかりやすくなるので注意が必要です。

せっかくなのでこれもモジュールにまとめておくことにします。

```openscad
module keycap_outer_shape (...) { ... }

module keycap_shape () {
  difference () {
    keycap_outer_shape(18, 14, 8);
    // 一回り小さいキーキャップ外形
    keycap_outer_shape(15, 11, 6.5);
  }
}

keycap_shape();
```

### 底面・上面を角丸にする / ミンコフスキー和 (minkowski)

ここまででおおざっぱな形状ができてきたので、ここからはディティールを作り込んでいくことにします。

ここまででできたモデルを見ていると、まず気になってくるのは、これがかなり角張っていることです。市販されている実際のキーキャップは、もう少し角が取れた形状になっているものが多いのではないかと思います。

立体の角を取るには、「ミンコフスキー和」と呼ばれる演算が便利です。これもまずは二次元で考えてみます。

<img width="654" alt="openscad_minkowski1.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/2fdb796d-534c-1ed8-6e02-c2a2cd08caa9.png">

上の図のように大きな四角と小さな丸があったとき、これらのミンコフスキー和は、次のブルーの線で囲まれた図形になります。

<img width="628" alt="openscad_minkowski2.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/29e8bdfc-43fa-8f9a-419b-6b6c9a4cae22.png">

イメージとしては、オレンジの図形（丸）の中心にぐさっと針を刺して、その針で黒の図形（四角）の上をなぞっていくとできる図形がミンコフスキー和です。例えばオレンジの図形が三角形だったらこんな感じになります。

<img width="613" alt="openscad_minkowski3.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/46e2f68f-df8e-1bff-438e-f2574ff458fa.png">

キーキャップの話に戻ると、これはいかにも角丸図形を作るのに使えそうです。

OpenSCAD には、二つの図形のミンコフスキー和を取るコマンド `minkowski` があります。せっかく「 `module` 」を覚えたので、試しにこれを使って角丸キューブを描画するモジュールを実装してみます。

直方体 `cube` は、円筒 `cylinder` とのミンコフスキー和を取ることで角丸になります。このとき、ミンコフスキー和を取る過程で、直方体が `cylinder` のサイズ分だけ膨らんでしまうため、あらかじめ元の直方体から その分だけ引いておきます。

<img width="411" alt="openscad_roundedcube.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/210082c6-7a63-89d9-f8c8-803b70e02449.png">

```openscad
$fs = 0.1;

module rounded_cube (size, r) {
  h = 0.0001; // cylinder の高さ (適当な値)
  minkowski () {
    cube([size[0] - r*2, size[1] - r*2, size[2] - h], center = true);
    cylinder(r = r, h = h);
  }
}

rounded_cube([10, 10, 10], 2);
```

あとは上のモデルの `cube` をそのまま `rounded_cube` に変えれば、角丸になったキーキャップのモデルができます。底面と上面で角丸の半径を変えるなどしても、可愛い形ができます。

<img width="409" alt="openscad_rounded_shape.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/3477f02b-3d85-4fe3-abaa-d272c7bbd9d5.png">

```openscad
$fs = 0.1;

module rounded_cube () { ... }

module keycap_outer_shape (key_bottom_size, key_top_size, key_top_height) {
  hull () {
    translate([0, 0, key_top_height])
      rounded_cube([key_top_size, key_top_size, 0.01], 3);
    rounded_cube([key_bottom_size, key_bottom_size, 0.01], 0.5);
  }
}

module keycap_shape () { ... }

keycap_shape();
```

### 傾きをつける / rotate

次は、キーキャップに傾斜をつけていきます。 DSA プロファイルや XDA プロファイルなどを除いて、一般的なキーキャップには行ごとに異なる傾斜がついています。

傾斜をつけるには、 `rotate` コマンドを使うのが簡単です。これは名前の通り、与えられた図形を任意の角度だけ回転させるコマンドです。 `rotate` の回転の起点は空間の原点になるので、 `translate` と併用する場合には注意が必要です (多くの場合、 `rotate` を先にするとうまくいくと思います)。

今回の用途では、キーキャップ外形の上面だけを回転させてやれば良さそうです。

<img width="411" alt="openscad_rotate.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/a603946c-008c-018e-cc8c-28a5dded201f.png">

```openscad
$fs = 0.1;

module rounded_cube () { ... }

module keycap_outer_shape (key_bottom_size, key_top_size, key_top_height, angle) {
  hull () {
    translate([0, 0, key_top_height])
      rotate([- angle, 0, 0])
        rounded_cube([key_top_size, key_top_size, 0.01], 3);
    rounded_cube([key_bottom_size, key_bottom_size, 0.01], 0.5);
  }
}

module keycap_shape () {
  difference () {
    keycap_outer_shape(18, 14, 8, 10);
    // 一回り小さいキーキャップ外形
    keycap_outer_shape(15, 11, 6.5, 10);
  }
}

keycap_shape();
```

### 窪みをつける / function

最後に指のフィット感に重要な、キーキャップの窪みをつけていきます。キーキャップの窪みには大きく分けて二つのタイプがあり、一つが円筒状に窪んでいる「Cylindrical Top」、もう一つが球状に凹んでいる「Spherical Top」です。

おそらく実物を見てみるのが早いと思います：

<img width="411" alt="openscad_cylindrical.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/ce376588-5886-866c-f3eb-f01df00b0bd2.png">

トップが円筒状に凹んでいる、 Cylindrical Top。 Cherry, OEM プロファイルなど、市販キーボードではこちらのタイプがメジャー。

<img width="411" alt="openscad_spherical.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/81899ba5-47c5-506d-5f9c-bef4c3b9050a.png">

トップが球状に凹んでいる、 Spherical Top。 SA, DSA, MDA プロファイルなど、沼の住民が買い集めるこだわりキーキャップではよく見かけるタイプ。

#### Cylindrical Top

まずは円筒状にトップを窪ませる、 Cylindrical Top なキーキャップを作ってみます。

例によって、キーキャップ外形から円筒を `difference` で抜き去ればトップを窪ませられるのはなんとなく想像できますが、ここで重要になってくるのが円筒の半径です。いったいどのくらいの半径に設定するとそれらしい形ができるのでしょう。

ここで中学数学が役に立ちます。

Cylindrical top なキーキャップは横から見ると、ざっくりこんな形をしています：

<img width="278" alt="cylindrical1.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/53735730-49ec-9efe-8b47-95997fab0df3.png">

円筒の半径を `r`、キーキャップ上面の幅を `w`、窪みの深さを `d` とすると、こんな図が書けます：

<img width="295" alt="cylindrical2.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/3cafe14e-27e6-d956-c8e3-5f87367acce8.png">

この三角形に対して三平方の定理を適用すると、等式

    r^2 = (r - d)^2 + (w / 2)^2

が得られ、これを (`r` について) 解くと

    r = (w^2 + 4 * d^2) / (8 * d)

となります。たとえば、これまで作っていたキーキャップのモデルは上面の幅 `w` が 11mm だったので、これを `d` = 1mm 窪ませて Cylindrical top にする場合、半径は

    r = (11^2 + 4 * 1^2) / (8 * 1)
      = 15.625 [mm]

とわかります。また、上の図を見ると、円の中心がキートップの上面からさらに `r - d` すなわち 14.625mm だけもちあげたところにくることもわかります。

OpenSCAD で は立体を返す `module` の他に、たんに数を返す `function` を定義することもできます。見通しをよくするためにも、この数式を関数として定義しておくことにします。

```openscad
function dish_r(w, d) = (w * w + 4 * d * d) / (8 * d);
```

実際にこの円筒をキーキャップ外形から取り去ってみます。

<img width="411" alt="openscad_cylindrical.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/445f3b61-8cfb-727c-e58c-0c138943990b.png">

```openscad
$fs = 0.1;
$fa = 0.25; // 大きな円の精度を上げる

module rounded_cube (...) { ... }

function dish_r(...) = ...;

module keycap_outer_shape (key_bottom_size, key_top_size, key_top_height, angle, dish_depth) {
  difference () {
    hull () {
      translate([0, 0, key_top_height])
        rotate([- angle, 0, 0])
          rounded_cube([key_top_size, key_top_size, 0.01], 3);
      rounded_cube([key_bottom_size, key_bottom_size, 0.01], 0.5);
    }
    translate([0, 0, key_top_height])
      rotate([- angle, 0, 0])
        translate([0, 0, dish_r(key_top_size, dish_depth) - dish_depth])
          rotate([90, 0, 0])
            cylinder(r = dish_r(key_top_size, dish_depth), h = 60, /* 適当に十分な長さ */ center = true);
  }
}

module keycap_shape () {
  difference () {
    keycap_outer_shape(18, 14, 8, 10, 1);
    keycap_outer_shape(15, 11, 6.5, 10, 1);
  }
}

keycap_shape();
```

簡単に解説します：基本的には、 `cylinder` を９０度回転して横向きにした上で、 `translate` で正しい高さまで持ち上げてあげて、`difference` で外形から抜き去ればそれらしいものになります。上で実装した「上面の傾いたキーキャップ」に対応するために、`translate` の過程で `rotate([- angle, 0, 0])` を挿入しています。これによって `translate` が２ステップに分断されてしまっていますが、この順番でやらないと正しい位置にきてくれないので注意してください　(入れ替えたらどうなるのかなど、実際に手元で実験してみてください)。また、ここで作る `cylinder` は径がかなり大きく、 `$fs = 0.1` としても精度が出ないため `$fa = 0.25` を指定しています。これは円を近似するときの、１パーツの最小の角度を指定するものです。

#### Spherical Top

トップを球形に窪ませる場合もやることは同じです。ただし、 Cylindrical Top の半径を決めるとき、キーキャップ上面の「幅」を基準に計算したのに対して、今回は対角線を基準に計算する (`sqrt(2)` 倍する) 必要があることに注意します。

<img width="411" alt="openscad_spherical.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/6fa69e69-a70b-5ea6-0526-b2944416f352.png">

```openscad
$fs = 0.1;
$fa = 0.25; // 大きな円の精度を上げる

module rounded_cube (...) { ... }

function dish_r(...) = ...;

module keycap_outer_shape (key_bottom_size, key_top_size, key_top_height, angle, dish_depth) {
  difference () {
    hull () {
      translate([0, 0, key_top_height])
        rotate([- angle, 0, 0])
          rounded_cube([key_top_size, key_top_size, 0.01], 3);
      rounded_cube([key_bottom_size, key_bottom_size, 0.01], 0.5);
    }
    translate([0, 0, key_top_height])
      rotate([- angle, 0, 0])
        translate([0, 0, dish_r(key_top_size * sqrt(2), dish_depth) - dish_depth])
          rotate([90, 0, 0])
            sphere(dish_r(key_top_size * sqrt(2), dish_depth));
  }
}

module keycap_shape () {
  difference () {
    keycap_outer_shape(18, 14, 8, 10, 1);
    keycap_outer_shape(15, 11, 6.5, 10, 1);
  }
}

keycap_shape();
```

## ステムを合体する / intersection

ステムと外形がそれぞれ完成したので、最後にこれらを合体させて一つのキーキャップにします。

コードの見通しをよくするために、ステムのモデルもモジュールでくくっておくことにします。

```diff
stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

module stem () {
  // 中身は上で作ったステムのモデルそのまま
  difference () {
    cylinder(d = stem_outer_size, h = 15);
    ...
  }
}
```

さて、まずは素朴に外形とステムを同じ空間に設置してみます。

<img width="409" alt="openscad_stem_union.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/521f6e2c-8776-c49d-4af9-e238d28f6fa4.png">

```openscad
$fs = 0.1;
$fa = 0.25;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

module stem () { ... }
module rounded_cube (...) { ... }
function dish_r(...) = ...;
module keycap_outer_shape (...) { ... }
module keycap_shape () { ... }

keycap_shape();
stem();
```

結構それっぽい感じにはなってきましたが、ステムが上面を突き抜けてしまっているのがマズい感じです。ここで、ステムの長さを微調整すればいいじゃんというのをまず思いつきますが、ここまででキーキャップ上面は斜めに傾いたり球状に窪んだりしてしまっているので、素朴にステムを縮めるだけではピッタリ接合しません。

あるオブジェクトを、別のオブジェクトにピッタリ収まるようにトリミングするのに便利なのが、 `intersection` です。

`intersection` は二つのオブジェクトの共通部分を取り出します。ベン図の円の重なっている部分を想像するとわかりやすいと思います。

<img width="366" alt="intersection.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/70af9936-be83-cfee-55d1-fb9b306c910e.png">

これを利用して、ステムをキーキャップ外形からはみ出さないようにトリミングできます：

<img width="409" alt="openscad_stem_trimmed.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/7038c8f3-d0eb-a3d6-d5a4-6ff86a7b8123.png">

```openscad
$fs = 0.1;
$fa = 0.25;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

module stem () { ... }
module rounded_cube (...) { ... }
function dish_r(...) = ...;
module keycap_outer_shape (...) { ... }
module keycap_shape () { ... }

intersection () {
  stem();
  keycap_outer_shape(18, 14, 8, 10, 2);
}
```

ステムと、中身をくり抜く前のキーキャップ外形の `intersection` をとります。

さて、ステムが無事トリミングできたら、これをキーキャップ外形と同じ空間に設置すれば、キーキャップモデルの完成です！

<img width="410" alt="openscad_keycap.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/71d3538c-f5fe-9d36-7472-6aef216abda6.png">

<img width="411" alt="openscad_keycap2.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/f3a7ac91-ce2f-7d9c-65f0-188994eb4f86.png">

```openscad
$fs = 0.1;
$fa = 0.25;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

module stem () { ... }
module rounded_cube (...) { ... }
function dish_r(...) = ...;
module keycap_outer_shape (...) { ... }
module keycap_shape () { ... }

intersection () {
  stem();
  keycap_outer_shape(18, 14, 8, 10, 2);
}
keycap_shape();
```

## 3D プリントする

ついに自分だけのキーキャップモデルが設計できたので、いよいよこれをプリントしていきます。

### キーキャップを並べて印刷用モデルを作る / for

家庭用の 3D プリンタで出力する場合などは別ですが、 DMM.make さんなど業者に出力を依頼する場合は、一度に１キーだけ出力するのは余りにコスパが悪いです。そこで、キーキャップのモデルをいくつか並べて、印刷用のモデルを作っていきます。

OpenSCAD には `for` という、およそ名前通りの機能を持ったコマンドがあります。これを利用すると、同じもしくは似たモデルを一度にたくさん出力することができます。たとえば

```openscad
for (x = [1, 2, 3]) {
  ...
}
```

というモデルは、

```openscad
x = 1;
...

x = 2;
...

x = 3;
...
```

と等価になります。

キーキャップのモデルを `for` で包んで、 `translate` によってずらしながら複数出力していけば、次のように、一度にたくさんのキーキャップをプリントするためのモデルができます：

<img width="407" alt="openscad_printmodel.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/983451b4-48c9-3649-3659-8ae494a75736.png">

```openscad
$fs = 0.1;
$fa = 0.25;

stem_outer_size   = 5.5;
stem_cross_length = 4.0;
stem_cross_h      = 1.25;
stem_cross_v      = 1.10;

module stem () { ... }
module rounded_cube (...) { ... }
function dish_r(...) = ...;
module keycap_outer_shape (...) { ... }
module keycap_shape () { ... }

module keycap () {
  // キーキャップのモデル
  intersection () {
    stem();
    keycap_outer_shape(18, 14, 8, 10, 2);
  }
  keycap_shape();
}

for (x = [0, 1, 2, 3, 4])
  for (y = [0, 1, 2, 3])
    translate([19 * x, 19 * y, 0])
      keycap();
```

ここでの注意点としては、キーキャップ間の距離が近すぎると、プリンタによってはひとつにくっついてしまう場合があります。この例ではキーキャップ間の距離を 19mm としています。底面の一辺を 18mm で作ったので、キーキャップ間には 1mm 程度の隙間が開くことになります。たとえば DMM.make さんの定番の素材で出力する場合、この程度あいていればおそらく十分です。

### stl ファイルのビルド

3D プリントにあたって次にやらなければいけないのが、 stl と呼ばれる 3D モデル形式のファイルを作ることです。

OpenSCAD の場合、 `Design > Render (F6)` でモデルをレンダリングしてから、 `File > Export > STL` で生成することができます。

ここでのレンダリングはプレビュー画像の生成よりも高精度で行われるので、モデルによっては非常に時間がかかる場合があります。特に複数のキーキャップを一度にまとめてプリントするようなモデルでは、非常に時間がかかります (かつて、ある特殊なキーキャップフルセットを作ろうとした時は、 Xeon マシンで 6 時間近くかかったこともありました)。

stl ファイルのビルドは以下のように、コマンドラインで行うこともできます。

    openscad -o "hogehoge.stl" "hogehoge.scad"

特にビルドに時間のかかるモデルでは便利かもしれません。

ビルド済みのモデルを複数まとめてプリントしたくなった場合には、フリーのツール Meshmixer で後から合成することもできます。

プリントしたいモデルをすべて stl からインポートし、それぞれのモデルについて、

- オブジェクトブラウザからモデルを選択
- `編集 > トランスフォーム` で回転・移動

を行って配置を調整したあと、今度はすべてのモデルを選択して `結合` を行います。合体できたら、 `エクスポート` から再び stl を出力できます。

### 3D プリント (DMM.make さんに依頼)

いよいよプリント用の stl ファイルができたので、発注をかけることができます。

ここでは DMM.make さんに依頼する場合の注意ポイントなどを紹介します。

※各種レギュレーション、料金システムなどは記事執筆時点のものです

#### モデルアップロードから発注まで

DMM のアカウントを作って DMM.make 3D プリントにログインすると、「3D データをアップロード」という動線が見つかります。これをクリックしてモデルをアップロードすると、データチェックが走ります。チェックに無事通れば、あとは素材を選んでカートに入れ、通販のように簡単に発注をかけることができます。

#### ファイルサイズが大きすぎる場合

DMM.make さんにアップロードできるモデルファイルには、容量の上限があります。一度にプリントしたいキャップの数が多い、あるいはキャップの形状自体が複雑、などの事情でこの上限を超えてしまった場合には、あらかじめモデルの容量を手元で削減する必要があります。

ここでも Meshmixer が便利です。

プリントしたい stl ファイルを Meshmixer でインポートし、すべてのポリゴンを選択 (`Command + a`)、 `編集 > 削減` と進むことでクオリテイを調整するダイアログが表示されます。ここでモデルの形状を損なわない範囲でポリゴン数を削減し、再度エクスポートするとプリント可能な容量になっているはずです (モデルによっては削減操作をしなくても、たんにインポートしてエクスポートするだけでいける場合もあります ... OpenSCAD と Meshmixer で出力する stl のバイナリ形式が違うため)。

#### 素材について

DMM.make さんではたくさんの種類の素材から選んでモデルをプリントすることができますが、費用対効果を考えるとおそらく

- ナイロン
- MJF

のどちらかが選択肢になってくると思います。

![Image from iOS.jpg](https://qiita-image-store.s3.amazonaws.com/0/54240/bf5e6dab-2fcb-2583-beab-988786a6feb4.jpeg)

ナイロンは非常に安いですが、かなり軽い素材で、キーキャップとしてはペラペラな質感になってしまいます。また、積層痕がある程度残ります。モデルの形状の確認用には使えますが、常用するには物足りなくなる可能性が高いと思います。

MJF はナイロンよりは高くなりますが、値段に対して質感がかなりよく、常用できるキーキャップとして現実的な素材です。 MJF にはさらに PA12 と PA12GB の二種類があり、後者は前者に比べてやや重量感のある強化品になります。この二種類では値段に差がないので、好みで選ぶことができます。「磨き」オプションなしでは表面がザラザラの質感になりますが、「磨き」オプションをつけることでツルツルになり、3D プリントとは思えないクオリティのものに仕上がります。

#### 「MJF ブラック（磨き）」で出力する場合の注意点

ここからは、おそらく 3D プリントで常用レベルのキーキャップを出力する場合のデファクトである「MJF ブラック（磨き）」特有の注意点について紹介していきます。

まず、出力可能サイズですが、 100mm * 100mm が限界になります。キーキャップでいくと、一列に並べられるのは５個が限度になります。代わりに立体配置が可能なので、２５個以上のキーキャップを一度に出力したい場合は、二層・三層と高さ方向にモデルを伸ばしていくことになります。

また出力可能な部品の数にも上限があり、 MJF の場合は 100 個までとなります。それ以上の数が必要になる場合は、半分サイズのモデルを二つ出力してください。

これは「磨き」オプション選択時特有の注意ですが、モデルによっては、細かい箇所があり破損のおそれがあるとして、確認のメールが届く場合があります。経験上、キーキャップのモデルを出力して致命的な破損に出くわしたことはありませんが、無保証にはなりますので、注意してください。破損リスクを理解の上、 DMM さんに感謝しながら、うちの子をよろしくお願いしますと返信しましょう。

#### 空間費のこと

家庭用の 3D プリンタで出力する場合、気になるのはフィラメントの消費量（＝総重量）です。一方、 DMM.make さんの場合、そのほかに「空間費」と呼ばれる概念があり、これが結構効いてきます。

空間費はプリントするモデルの「最外形寸法」から計算されます。したがって、たとえば出力するキーキャップの数が同じでも、ギチギチに詰まったモデルとスカスカのモデルでは、前者の方が安く出力することができます。

例：２０個のキャップをプリントする、効率の悪いモデル

<img width="409" alt="openscad_bad_price.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/73dac5bc-b260-27d8-2f16-df01294e5d79.png">

効率の良いモデル

<img width="408" alt="openscad_good_price.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/4ff1e891-88f8-a9b0-4eac-35116921cccc.png">

## まとめ

この記事では、キーキャップの形状の作り込み、ステムの接合、それに必要な CAD の知識を紹介しました。

これらの知識だけでも、結構自由にキーキャップのモデルを作れると思います。例えば私が最近試している、親指用キーキャップのモデルはこんな形をしています：

<img width="412" alt="openscad_thumbkeys.png" src="https://qiita-image-store.s3.amazonaws.com/0/54240/2bb82e5a-95c6-0099-92ca-edab4eeee8bb.png">

指の当たる部分がぐにっと曲がってフィットするほか、 0.125u 分だけ長めに作られていて、親指が届きやすくなっています。

あるいはちょっと頑張ればこんな奇抜なキーキャップも作れます（半分ネタで、まだ実際には出力していません）。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">LED透過キーキャップ（物理） <a href="https://t.co/oZ2KClubY1">pic.twitter.com/oZ2KClubY1</a></p>&mdash; ぜろけー🔕 (@zk_phi) <a href="https://twitter.com/zk_phi/status/1067618765037166592?ref_src=twsrc%5Etfw">2018年11月28日</a></blockquote>

六角柱を `for` でたくさん並べて、キーキャップから `difference` で抜き去る」という方法で作っていて、ここで紹介した知識でも十分作れるものです。

どんな立体をどういう風に組み合わせたらあれが作れるんだろう…？と、パズル感覚で楽しみながら、今までにない形状のキーキャップの研究が盛り上がったら嬉しいなあと思います！

明日の記事は ergoinu の hsgw さんです！

この記事は MBP 内蔵キーボードで書きました :sob: