Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

React Native製アプリのフォントサイズをいい感じに設定したい

そとあそび所属の天野(@mutachii) です。

この記事は Akatsuki Advent Calendar 2018 の17日目の記事です。
前回は s-capybaraさんの、Elixir でソースコードジェネレーションする - Qiitaでした。

はじめに

React Nativeでの開発で避けて通れないのは、多種多様なスクリーンで表示崩れを発生させないように実装することです。

幸い、Flexboxのサポートのおかげでレイアウトの実装で困ることは少ないですが、対応が面倒なのが fontSize の指定だと思います。
これらは当然 Flexbox では対応することができないし、widthやheight, paddingなどのようにパーセント指定することもできません。

React Native 開発始めたてのときにあるある(だと思っている)のは、普段は iPhoneX で動作確認している開発者が、
いざリリースするぞとなったタイミングで、iPhone8やiPhoneSEでは文字が大ぎることや、 逆に、iPadで確認すると文字が異様に小さいということに気づき対応に追われるということじゃないでしょうか...

今回は、そのfontSize 問題の解決に有用な

という2つのライブラリを紹介します。

react-native-size-matters

以下のように使うライブラリです。

import { scale, verticalScale, moderateScale } from 'react-native-size-matters';

const Component = props =>
  <View style={{
    width: scale(30), 
    height: verticalScale(50),
    padding: moderateScale(5)
  }}>
    <Text style={{
      fontSize: moderateScale(14)
    }}>すごい</Text>
  </View>

このscaleやverticalScaleがやっていることはすごく単純で、 基準となるスクリーンサイズ(350 x 650)に対する、現在のスクリーンサイズの倍率を算出して、その倍率をかけた数値を返してくれます。 それだけなので、fontSizeはもちろん、widthやheightにも利用できます。

moderateScaleだけはすこし特殊で、

Sometimes you don't want to scale everything in a linear manner, that's where moderate scale comes in.
The cool thing about it is that you can control the resize factor (default is 0.5).
If normal scale will increase your size by +2X, moderateScale will only increase it by +X, for example:
➡️ scale(10) = 20
➡️ moderateScale(10) = 15
➡️ moderateScale(10, 0.1) = 11

というように、増加 / 減少 させるときの係数を指定できます。 スクリーンサイズが2倍になったからといって、すべての要素のサイズを2倍にしたいわけではない...というようなケースで使います。

微妙に問題になるのは、基準となるスクリーンのサイズが 350 x 680 という値なので、例えば デザインデータが iPhoneXで作成されている場合は、デザインどおりに実装できないことです。
とはいえ、上に書いたとおりとてもシンプルな実装なので、自分たちの基準となるスクリーンサイズを使って実装し直すという方法もありそうです。

react-native-responsive-fontsize

名前通りに responsive な fontSize の指定を可能にする RF という関数を提供してくれます。

import RF from "react-native-responsive-fontsize"

const styles = StyleSheet.create({
  welcome: {
    fontSize: RF(3.5),
    textAlign: "center",
    margin: 10
  },
  instructions: {
    textAlign: "center",
    color: "#333333",
    marginBottom: 5,
    fontSize: RF(2.5)
  }
})

こちらの実装もシンプルでデバイスのHeight を基準にフォントサイズを決めます。なので、RFの引数は fontSize ではなくて パーセンテージ であることに注意してください。

さいごに

fontSize問題にすぐ効く2つのライブラリを紹介しました。

基本的には薄い実装のライブラリ達なので、実装の方向性を確認して、自分たちのプロダクトに合わせて改変していくことや、
PRを出してライブラリ自体を改善しやすいのかなと思っています。

CodeBuildでサーバレスバッチ環境を運用する

この記事は Akatsuki Advent Calendar 2018 の15日目の記事です。 前回は sejimhpさんの、環境改善の必要性〜真っ白な Terminal から tmux へ移行〜 でした。

はじめに

システムを運用する上で、安価で運用負荷の少ないバッチ処理を設計したいニーズは多いと思います。そもそもバッチ処理自体を極力除外したいのですが、日々の運用ツールなども含めると、どうしても実装しなければならない場面が出てきます。

本記事では

  • 安く
  • サーバレスで
  • 並列性高く
  • スケジューラでもイベントドリブンでも

実行できる、AWS上でのバッチ環境の実装の話をしたいと思います。

よくあるAWSバッチ処理アーキテクチャパターン

AWSバッチ処理アーキテクチャパターンBest Practice for Online Game Development on AWS が詳しいです。これらのアーキテクチャに対して私見をまとめると

EC2パターン

  • Pros: シンプルで使いやすく、速やかに実装ができる。
  • Cons: EC2料金が常にかかる。環境変数などの運用管理も必要。

Lambdaパターン

  • Pros: サーバレス。AWSのリソースを扱うには便利。
  • Cons: ゲームアプリケーションのロジックなどは使い回せない。タイムアウトも短め。

ECSパターン

  • Pros: CodePipeline等のパイプラインに組み込める。
  • Cons: 結局EC2が必要で、かつバッチで使うには比較的複雑。

Fargateパターン

  • Pros: サーバレス。
  • Cons: コストが割高で、プロビジョニングに1〜2分かかる。

例を挙げればAWS Batchなどのサービスもあるのでキリがないのですが、結局似たようなProsConsになるかと思います。今回は、これらのアーキテクチャを使わず、CodeBuildでバッチ処理を実装をする話をしていきます。

CodeBuildとは?

AWSが提供するマネージドビルド環境で、buildspec.ymlに記載した通りにビルドをすることができます。  

よくあるCodeBuildの使い方

通常、docker build用の環境などに用います。 弊社でもdocker buildが主な用途で、CodeBuildでdocker build後、ECRへdocker pushし、ECS / Fargateへデプロイするために使用しています。

f:id:e__koma:20181214155303p:plain
CodeBuildの使用例

buildspec.ymlの例は以下のとおりです。 パラメータストアと連携して秘匿情報をセキュアに一元管理したり、ビルド済みのdocker imageを実行環境として使えるところも嬉しいです。

version: 0.2

env:
  parameter-store:
    AWS_ACCESS_KEY_ID: "project-id.aws.access.key.id"
    AWS_SECRET_ACCESS_KEY: "project-id.aws.secret.access.key"

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
      - REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}
  build:
    commands:
      - echo Build started on $(date)
      - echo Building the Docker image...
      - |
        docker build -t ${REPOSITORY_URI}:latest -f ${DOCKERFILE_PATH} . \
        --build-arg YOUR_ENV=${YOUR_ENV}
  post_build:
    commands:
      - echo Build completed on $(date)
      - echo Pushing the Docker images...
      - docker push ${REPOSITORY_URI}:latest

CodeBuildを使用したバッチ環境

さて、本題です。 CodeBuildを使ってバッチ処理を実行する方法を説明します。

パターン1:バッチスクリプト実行

CodeBuildはビルド環境に様々な言語が用意されており、かつVPC内で実行することができるため、RubyPythonスクリプトを実行することができます。

f:id:e__koma:20181214170023p:plain
CodeBuildでスクリプト実行例
 

実行後はCloudWatchEventsでイベントフックをして、slack通知をしてもいいですし、Lambdaで別の処理に移行するのも良いでしょう。CodePiplineやJenkinsのAWS CodeBuild Pluginを使用したPipelineに組み込めば、前後関係を豊富にカスタマイズできます。

この場合のbuild_spec.ymlは非常にシンプルに書けます。

version: 0.2

env:
  parameter-store:
    YOUR_ENV: "your.env"

phases:
  build:
    commands:
      - python ./test.py
artifacts:
  files: test.log

 

パターン2:ビルド済みアプリケーションの実行

  パターン1ではスクリプトを実行するだけでしたが、ビルド済みのアプリケーションを使った運用ツールを実行したい場合があります。例えばデータベースのmigration、 ゲームアプリケーションロジックを用いたアイテムドロップシミュレータなどです。

この要望を満たすためには、ビルド済みのアプリケーションImageを再度CodeBuildにpullしてこればOKです。

f:id:e__koma:20181214171045p:plain
CodeBuildでビルド済みアプリケーションの実行例

この場合、build_spec.ymlには少し工夫が必要になります。 ビルド済みのImageをpullしてきた後、docker runをする際にENTRY_POINTを空にして、実行したいコマンドを渡します。 こうすることでENTRY_POINTを無視して、ビルド済みのアプリケーションに対してコマンドを実行することができます。

version: 0.2

env:
  parameter-store:
    YOUR_ENV: "your.env"

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
      - REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}
  build:
    commands:
      - echo started on $(date)
      - docker pull ${REPOSITORY_URI}:latest
      - echo ${BATCH_NAME} starting ...
      - |
        docker run \
        -e YOUR_ENV=${YOUR_ENV} \
        --entrypoint="" -i ${REPOSITORY_URI}:latest \
        sh -c "${BATCH_COMMAND}"
  post_build:
    commands:
      - echo ${BATCH_NAME} completed on $(date)

さて、ここまで説明してきたCodeBuildでのバッチ処理のメリットをまとめます。

機能性

  • ビルド済みのアプリケーションロジックを使い回せる
  • VPC内で実行できるためEC2 / RDSなどへのアクセスができる
  • 並列性が高い。
  • リソースの範囲内であれば長時間バッチも可能

運用性

  • サーバレス
  • 成功・失敗のイベントフックが豊富
  • Pipelineへの組み込みが容易なため前後関係の定義ができる

コスト

  • 従量課金のため、とにかく安い。

デメリットを挙げるとすれば以下のとおりです。

デメリット

  • プロビジョニングに30秒ほどかかる(ただしFargateのプロビジョニングよりかは速い)
  • 使える最大リソースは8vCPU、15GBメモリなので、これ以上のパワーが必要な処理ではリソースが足りない。

まとめ

CodeBuildを使ったバッチ処理を紹介してきました。 CodeBuildをバッチ環境として使い始めたプロジェクトでは、バッチサーバという概念がなくなりました。 興味がある人は、ぜひ使ってみてください!

GAE/Go & CircleCI でカナリアリリースをする

この記事はAkatsuki Advent Calendar 2018の12日目の記事です。 前回は hayamaruさんの、知覚メカニズムと網膜投影でした。

はじめに

継続的デリバリーという文脈で、素早く、安全にデプロイする手法について、近年多くの記事を見かけます。

例えば、カナリアのおかげで命拾い : CRE が現場で学んだことカナリアリリースについて詳しく触れられています。このアイデアは「たとえテスト環境で十分な検証をしたとしても、本番環境で問題が発生しないということを保証するのは難しいので、少しづつトラフィックを移動して問題があったらすぐにロールバックをしよう」というものです。

上記記事中に紹介されているKayentaといったツールを使ってカナリア分析を行い、自動的にカナリアリリースするというのも良いですが、人間による承認を元にデプロイするならもう少しコストの低い方法で良さそうです。

この記事では、CircleCIのワークフローを使って、承認を元にGoogle App Engineトラフィックを移動するという形で、カナリアリリースを実現します。

サンプルアプリケーション

この記事のサンプルコードはcsouls/gae_go111-deploy_sampleに置いています。 また、サンプルプロジェクト名をproject-nameとしているので、適宜読み替えてください。

(このコードは、Google Cloud Platformのリポジトリで公開されているGAEのサンプル、go11x/warmup からコピーして修正したものです。)

このアプリケーションは、初期処理が完了した後に、どれだけ時間が経過しているか?というのを表示するための実装と、ウォームアップリクエストを受け付けるための実装がされています。

GAEでのデプロイ

Google App Engineへのデプロイはとても簡単です。 Cloud SDK をインストールして、GCPコンソールから新しいプロジェクトを作り、以下のコマンドを実行したあとにデプロイするリージョンや確認に応答するだけで、アプリケーションのデプロイができてしまいます。

gcloud app deploy --project=project-name

結果

You are creating an app for project [project-name].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed. More information about regions is at
<https://cloud.google.com/appengine/docs/locations>.

Please choose the region where you want your App Engine application
located:

 [1] asia-east2    (supports standard and flexible)
 [2] asia-northeast1 (supports standard and flexible)
 [3] asia-south1   (supports standard and flexible)
 [4] australia-southeast1 (supports standard and flexible)
 [5] europe-west   (supports standard and flexible)
 [6] europe-west2  (supports standard and flexible)
 [7] europe-west3  (supports standard and flexible)
 [8] northamerica-northeast1 (supports standard and flexible)
 [9] southamerica-east1 (supports standard and flexible)
 [10] us-central    (supports standard and flexible)
 [11] us-east1      (supports standard and flexible)
 [12] us-east4      (supports standard and flexible)
 [13] us-west2      (supports standard and flexible)
 [14] cancel
Please enter your numeric choice:  2

Creating App Engine application in project [project-name] and region [asia-northeast1]....done.
Services to deploy:

descriptor:      [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample/app.yaml]
source:          [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample]
target project:  [project-name]
target service:  [default]
target version:  [20181212t155052]
target url:      [http://project-name.appspot.com]


Do you want to continue (Y/n)?  y
... deploy log ...

Deployed service [default] to [http://project-name.appspot.com]

デプロイの中身

gcloud app deploy を実行することで何が行われているかは、コマンドを実行したあとの標準出力に表示されています。

Services to deploy:

descriptor:      [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample/app.yaml]
source:          [/Users/y.tanaka/src/sandbox/gae_go111_deploy_sample]
target project:  [project-name]
target service:  [default]
target version:  [20181212t160030]
target url:      [http://project-name.appspot.com]

この例では、gae_go111_deploy_sample/app.yaml という App Engineの設定を元に、gae_go111_deploy_sampleをソースとして、project-nameというプロジェクトのdefaultというサービスの、バージョン20181212t160030を作成し、そのバージョンでサービスのトラフィックを100%処理する設定にした上で、http://project-name.appspot.com というURLで公開する、という内容となっています。

gcloud app deploy に、--no-promoteというフラグを指定すると、「新しいバージョンは作るが、トラフィックは移動しない」という動作になります。

デプロイのデフォルト動作

gcloud app browse --project=project-name を実行してブラウザでアクセスしてみると、実装の通りインスタンス起動からの経過時間が表示されます。表示されている時間は約50msということなので、このインスタンスに対する最初のアクセスであることが分かります。 f:id:csouls:20181212231737p:plain

再度アクセスすると、初回アクセスからの経過時間が表示され、同じインスタンスで要求を受けていることが分かります。 f:id:csouls:20181212231949p:plain

GAEのコンソール、Logging -> ログから、Stackdriver Logging に自動的に記録されたアクセスログが確認できます。レイテンシの内容を確認すると、初回アクセスの応答は50msではなく、775msかかっているようです。体験としてもこれくらい待たされています。

また、トレースがサンプリングされている場合は、レイテンシをクリックすることでトレースの詳細を確認することができます。

f:id:csouls:20181212232051p:plain

どうやら、サービスのデプロイにより新しいプロセスが起動され、loading requestが発生しているようです。 f:id:csouls:20181212232159p:plain

このサービスではapp.yamlに以下の記載をしているので、warmup requestsが有効になっていますが、単純に gcloud app deployをしても、warmupが実行されない様です。

inbound_services:
  - warmup

handlers:
  - url: /_ah/warmup
    script: auto
    login: admin

本番環境でデプロイの運用をする場合は、この動作についても何らかの対処が必要でしょう。

CircleCIの設定

デフォルトの動作を踏まえ、warmupを有効にする形で GAE & CircleCIでのカナリアリリースを設定していきます。

サービスアカウントの設定

この手順 に従い、circleci/gcp-cli@1.0.1を使う前提で、サービスアカウントの設定をします。気をつけることは以下です。

  1. GAE コンソールから、App Engine Admin API を有効にする必要があります。
  2. GAEでデプロイをさせるサービスアカウントには、「App Engine デプロイ担当者」「App Engine サービス管理者」「Cloud Build サービス アカウント」「ストレージのオブジェクト管理者」の権限が必要です。
  3. CircleCIのプロジェクト設定 -> BUILD SETTINGS -> Environment Variables -> Add Variable から、下表の環境変数を設定します。
環境変数 内容
GCLOUD_SERVICE_KEY base64 <service-account-json>.json の結果
GOOGLE_PROJECT_ID GCPプロジェクトのID
GOOGLE_COMPUTE_ZONE 動作させるZone。空白を設定すると、Zone指定なし。※後続の項で説明するcircleci/gcp-cliを使う場合、環境変数の定義は必ずしておかなければいけない。

gcp-cli/initializestepのパラメータで環境変数名を指定することができるので、一つのリポジトリに対して複数の環境があっても大丈夫です。

CircleCIの設定ファイル

リポジトリ直下の.circleci/config.yml に、以下の様に記述します。

version: 2.1
orbs:
  gcp-cli: circleci/gcp-cli@1.0.1
executors:
  golang:
    docker:
    - image: circleci/golang:1.11
  gcloud:
    docker:
    - image: google/cloud-sdk:latest

jobs:
  test:
    executor: golang
    steps:
      - checkout
      - restore_cache:
          keys:
          - go-mod-v1-{{ checksum "go.sum" }}
      - run:
          name: build
          command: go build
      - run:
          name: test
          command: go test

  deploy:
    executor: gcloud
    steps:
      - checkout
      - gcp-cli/initialize
      - run: gcloud app deploy --no-promote --version $CIRCLE_SHA1 --quiet

  set-traffic:
    parameters:
      before-traffic:
        type: string
        default: "0.99"
      after-traffic:
        type: string
        default: "0.01"
    executor: gcloud
    steps:
    - checkout
    - gcp-cli/initialize
    - run: |
        BEFORE_VERSION="$(gcloud app versions list --service=${GAE_SERVICE} --filter='traffic_split>0.5' --format='value(id)')"
        gcloud app services set-traffic --splits ${BEFORE_VERSION}=<< parameters.before-traffic >>,${CIRCLE_SHA1}=<< parameters.after-traffic >> --split-by=random --quiet

  promote:
    executor: gcloud
    steps:
    - checkout
    - gcp-cli/initialize
    - run: gcloud app services set-traffic --splits ${CIRCLE_SHA1}=1 --split-by=random --quiet --migrate

workflows:
  version: 2
  test_and_deploy:
    jobs:
      - test
      - deploy:
          requires:
          - test
          filters:
            branches:
              only:
                - master
      - hold-canary:
          type: approval
          requires:
          - deploy
      - set-traffic:
          name: canary
          requires:
          - hold-canary
          before-traffic: "0.01"
          after-traffic: "0.95"
      - hold-promote:
          type: approval
          requires:
          - canary
      - promote:
          requires:
          - hold-promote

以下、ポイントを解説します。

orbs

orb とは、CircleCI 2.1から追加された、設定をパッケージとして共有可能にするものです。 今回はGCP CLIを使うので、その初期設定でcircleci/gcp-cli@1.0.1を利用しています。

orbs:
  gcp-cli: circleci/gcp-cli@1.0.1

...snip...

jobs:
    steps:
      - gcp-cli/initialize

deploy ジョブ

新しいバージョンを作るジョブです。 $CIRCLE_SHA1をバージョン名として指定しています。これで、CircleCIから操作するバージョンを特定するのが簡単になります。

  deploy:
    executor: gcloud
    steps:
      - checkout
      - gcp-cli/initialize
      - run: gcloud app deploy --no-promote --version $CIRCLE_SHA1 --quiet

set-traffic ジョブ

トラフィックの分割をするジョブです。 トラフィックの50%以上を割り当てられているバージョンを前のバージョンだと判断しています。

  set-traffic:
    parameters:
      before-traffic:
        type: string
        default: "0.99"
      after-traffic:
        type: string
        default: "0.01"
    executor: gcloud
    steps:
    - checkout
    - gcp-cli/initialize
    - run: |
        BEFORE_VERSION="$(gcloud app versions list --service=${GAE_SERVICE} --filter='traffic_split>0.5' --format='value(id)')"
        gcloud app services set-traffic --splits ${BEFORE_VERSION}=<< parameters.before-traffic >>,${CIRCLE_SHA1}=<< parameters.after-traffic >> --split-by=random --quiet

promote ジョブ

トラフィックを新しいバージョンに100%移行するジョブです。 --migrate オプションを付けることで、トラフィックを徐々に移行してくれます。 また、Warmup処理を実行してくれます。

promote:
  executor: gcloud
  steps:
  - checkout
  - gcp-cli/initialize
  - run: gcloud app services set-traffic --splits ${CIRCLE_SHA1}=1 --split-by=random --quiet --migrate

ワークフロー

test -> deploy -> hold-canary -> set-traffic(canary) -> hold-promote -> promoteの順に並ぶように、requiresで設定しています。

また、deployはmasterブランチでのみ実行されるので、それ以外のブランチではtestだけが実行されます。

hold-canary, hold-promote では type: approval で設定しているため、ここで人間の承認が入るという設定になっています。

workflows:
  version: 2
  test_and_deploy:
    jobs:
      - test
      - deploy:
          requires:
          - test
          filters:
            branches:
              only:
                - master
      - hold-canary:
          type: approval
          requires:
          - deploy
      - set-traffic:
          name: canary
          requires:
          - hold-canary
          before-traffic: "0.01"
          after-traffic: "0.95"
      - hold-promote:
          type: approval
          requires:
          - canary
      - promote:
          requires:
          - hold-promote

結果

masterブランチにpushしたあとに、deployジョブ(新バージョンの作成)が完了した状態です。 type: approvalのワークフロー定義により、hold-canaryで止まっていることが分かります。 f:id:csouls:20181212233504p:plain

hold-canaryを承認し、canaryジョブまで実行された結果です。 ワークフローの承認者がアイコンで表示されていること、次のhold-promote で止まっていることが分かります。このジョブの前に、承認待ちのワークフローへのリンクをSlackへ通知するのも良いでしょう。 f:id:csouls:20181212233819p:plain

トラフィックの割当も新バージョンに1%, 旧バージョンに99%となっています。 この状態でアプリケーションエラーが発生していないか、新バージョンのレイテンシやその他メトリクスに悪化したものがないか、を確認することができます。 f:id:csouls:20181212233950p:plain

承認する際は、このようなダイアログが表示されるので、間違えてクリックしてしまっても大丈夫です。 f:id:csouls:20181212233236p:plain

--migrate オプションを付けたことで、トラフィックが徐々に移行されていることが分かります。 f:id:csouls:20181212233247p:plain

Stackdriver loggingからログをみても、warmup リクエストが処理されており、2回目の応答は4ms程度で返せていることが分かります。 f:id:csouls:20181212233311p:plain

開発者の数が50名を超えるような大規模なプロジェクトになると、冒頭で紹介したKayentaのようなツールが必要になると思いますが、小規模であればこのCircleCIのワークフローを使った人間による承認でも十分なことが多いと思います。

設定にコストをかけずに、GAEのカナリアリリースを実現する手段として、CircleCIと組み合わせるアイデアを覚えておくと役に立つことがあるかもしれません。

Git のベースブランチを記録する

この記事は Akatsuki Advent Calendar 2018 の 8 日目の記事です。

このブログでは初めましてになります。id:thinca です。今日は Vim の話…ではなく、お仕事用にごにょごにょしている Git の設定の話をしようかと思います。

背景

私のいるチームでは複数バージョン平行開発が行われており、それもあいまって Git リポジトリには各バージョンや stagingmaster など様々なブランチが存在しています。

そしてそれぞれのブランチから作業用のブランチを作るわけですが、Git では「あるブランチがどのブランチから派生したのか」ということは記録されていません*1。Git の構造を考えると記録されていないのは当然なのですが、これを記録するようにすると、様々な場面で活用できます。そこで私が普段している工夫を紹介したいと思います。

Git の alias 機能

この工夫のために、Git に大量の alias を定義しています。今回の話はほぼ、この alias 集の紹介になります。

各 alias は別の alias に依存しているものもあるので、順を追って紹介していきます。

以後出てくる xxx = yyy の形式の行は、Git の config ファイル内での記述になります。

現在のブランチを取得する

current-branch = symbolic-ref --short HEAD

ありそうでない、現在のブランチを表示するコマンドです。様々なシーンで使うので、最初に紹介しておきます。

ブランチ元のブランチを記録/表示する

base-branch = !git config "branch.$(git current-branch).base"

本稿で伝えたい工夫のコアの部分になります。git config を使って、各ブランチの base と言う名前の項目にブランチ元のブランチ(=base branch)を記録できるようにします。 以下のような感じで使います。

$ git current-branch
new-feature

# ベースブランチを master に設定する
$ git base-branch master

# ベースブランチを表示する
$ git base-branch
master

$

とは言っても、実際にこのコマンドを直接使うことはあまりありません。他の alias から利用する場合が多いです。

ちなみに、こうして記録した内容は .git/config ファイル内に記録されます。ブランチを消したら設定も一緒に消えるので、ゴミが残ったりはしません。

[branch "new-feature"]
    base = master

作業用ブランチを作成する

現在のブランチから作業用ブランチを作成する際、私はよく git cob という alias で行っていました。これは最初は git checkout -b の略だったのですが、base-branch を導入した今では以下のようになっています。

cob = "!f() { \
  b=$(git current-branch) && \
  git checkout -b \"${1}\" && \
  git base-branch \"${b}\"; \
}; f"

alias で引数を受け取りつつアレコレしたい場合のイディオムの 1 つとして、関数を作ってこれを実行しています。他にも方法はありますが、私はこの方法をよく使います。

これで、git cob new-feature を実行すると、new-feature ブランチのベースブランチとして元いたブランチが記録されるようになりました。

分岐地点のコミットを得る

せっかく記録をしても活用しないと意味がありません。そのためにも、追加の補助機能を用意します。

base-commit = !git merge-base origin/$(git base-branch) HEAD
     -o--o  <- new-feature
    /
o--o--o--o  <- master
   ^
   ここを得る

こんな感じで、現在のブランチを切った根本に当たるコミットの sha1 値を得るコマンドになります。これがあると、以下のようなことができます。

現在のブランチ内だけのログを表示

普通にログを表示すると、辿れるだけ際限なく表示されます。今のブランチでは何をやったのかに注目したい場合、その部分だけ表示できると便利です。

fl = !git log $(git base-commit)..HEAD --reverse

ログの表示の仕方は好みがあると思うので、お好みの感じにするとよさそうです。alias の名前も短くて覚えやすいのを付ければよいかと思います。

現在のブランチで変更したファイルを表示

先ほどと同じようなやつですね。これは diff でできます。

bf = !git diff --name-only "$(git base-commit)..HEAD"

変更したファイルだけ、まとめて lint にかけたりする際に便利です。

$ rubocop $(git bf)

特定のバージョンブランチから派生した作業用ブランチ一覧を表示する

あちこちのバージョンで気まぐれに始めては途中で止まっているブランチがあるため、通常の git branch によるブランチ一覧だとどのブランチがどのバージョン上で作業していたものなのかがわかりづらくなってしまいます。そこで、今いる(あるいは指定した)ブランチから作られたブランチの一覧を表示できるようにします。

features = "!f() { \
  git branch | \
    cut -c3- | \
    while read b; \
      do echo \"$(git config \"branch.${b}.base\") -> ${b}\"; \
    done | \
    grep -v '^ -> ' | \
    grep \"^${1:-$(git current-branch)} ->\" | \
    sort; \
}; f"

こういう感じで結果が見れます。

$ git features master
master -> feature-a
master -> feature-b

$

rebase する

当然 rebase も簡単にできます。

rbu = !git rebase "origin/$(git base-branch)"
     -o--o  <- new-feature
    /
o--o--o--o  <- master

 |
 v

           o--o  <- new-feature
          /
o--o--o--o  <- master

fetch によって origin が伸びている場合が多いので、origin のブランチの先頭に対して rebase するようにしています。 実は git base-branch でもこっそり origin を見ていました。これは今回のように rebase することがあるためです。

base を移動する

version-a ブランチからブランチを作ってやってる作業を version-b に入れることになった!となったら、rebase を利用すればサクッと移動できます。

move-base = "!f() { \
  [[ -n $1 ]] && \
  git rebase --onto $1 $(git base-branch) && \
  git base-branch $1; \
}; f"

git move-base version-bversion-b に移動できます。

現在の差分をブランチ中の特定のコミットに混ぜ込む

ブランチで作業中に、途中で typo を入れ込んだことに気付いた時、新たに Fix typo なんてコミットを積む人はいないと思います。後から見て履歴がわかりやすくなるように、最初から typo を入れないように履歴を修正するでしょう。となったら rebase による履歴改変の出番となるわけですが、普通にやると意外に面倒です。これもサクッとできるようにしています。

まずは混ぜ込む先のコミットを簡単に選べるようにしたい、ということで以下は fzf を使ってコミットを選択するコマンド。コミットの範囲を引数に渡します。previewgit show を使ってコミットの中身を見つつ選択できるようにしてあります。

select-commit = "!f() { \
  git log --oneline ${1} | \
    fzf --preview='git show --color {1}' | \
    cut -b 1-7; \
}; f"

続いて現在の差分を、この git select-commit を使って選択したコミットに対する fixup コミット*2としてコミットするコマンド(わかりづらい)が以下。

fixcommit = "!f() { \
  git commit --fixup=\"$(\
    git select-commit \"${1:-$(git base-commit)}..HEAD\"\
  )\"; \
}; f"

git base-commit を使って適切な範囲のみを候補として表示するようにしています。

さて、fixup コミットを作ったので、これを適用します。適用するためのコマンドが以下。

fixup = !git -c core.editor=true rebase -i --autosquash $(git base-commit)

base を記録しているおかげで、rebase を機械的に実行できます。--autosquash を付ければ最初から fixup 済みの状態でエディタが開くので、編集する必要もありません。編集しなくてもよいので、エディタとして true コマンドを指定して、すぐに閉じています。これで fixup を適用できます。

ここまで来たらまとめてやってしまいたいので、まとめてやるコマンドを用意しています。

fix = "!f() { \
  git fixcommit ${1} && \
  git fixup; \
}; f"

つまり、typo 直したい!となったら、

  1. 直して git add
  2. git fix
  3. fzf が起動するので対象コミットを選択
  4. 混ぜ込み完了

となります。簡単ですね。

GitHub の PR 作成ページを開く

近ごろの GitHub はとても便利なので、新規ブランチを push すると「このページにアクセスすれば PR 作れるよ!」と教えてくれます。*3

remote: Create a pull request for 'new-feature' on GitHub by visiting:
remote:      https://github.com/aktsk/awesome-product/pull/new/new-feature

しかし、いざこのページにアクセスすると base はデフォルトブランチとしてページが開きます。デフォルトブランチと開発ブランチでは差が大きくなっていることは珍しくなく、差分の生成のために GitHub を開くのが重くなります。こんなページは開きたくない。

そこで、直接!目的の base に対して PR が作れるページをブラウザで開くコマンドです。ブラウザを開くために open を使っているので、MacOS 専用です。

gh-repo = "!git remote get-url origin | sed 's/.*:\\(.*\\)\\.git/\\1/'"
mkpr = "!open https://github.com/$(git gh-repo)/compare/$(git base-branch)...$(git current-branch)?expand=1"

まとめ

今回は、Git にベースブランチを記録することで様々な作業を効率化する例を紹介しました。Git の alias の設定は config ファイル 1 つで管理できるため、環境間での取り回しがやりやすいです。その分複雑なことをしようとするとメンテナンスが難しくなってしまいますが、その点は適切に改行を入れたり、コマンドを分割することで緩和してみました。

小さな工夫で作業を効率化できることは多いので、不便に感じたら積極的に効率化して行きましょう。

*1:単純なリポジトリであればある程度推察できる場合がありますが、複雑になると失敗するので確実性がありません。

*2:fixup コミットについてはこちらの記事等が参考になります。

*3:この URL は例なので実在しません。

GLSL SandBoxで手軽にレイマーチングで遊ぼう

この記事は Akatsuki Advent Calendar 2018 - Adventar 1日目の記事です。

はじめに

 こんにちは。クライアントエンジニアのRiyaaaaaです。

 皆さんは最近シェーダーを書いていますか? 私はあんまり書いていないですね。
 長らく触れないと、カンが鈍りがちになりますよね。なので、定期的に簡単なものでも書いたり作ったりすることは脳の体操にも効果的です。

 しかし、いざシェーダーを書こう!と思っても、ゲームエンジニアにとってはシェーダーはゲームやレンダラーと密接に結びついていて、手軽に何かを試すというのは難しかったりします。UnityやUnreal Engine4をわざわざ起動するのも億劫ですよね。
 そんな時、GLSL(OpenGL Shading Language)をWeb上でコーディングできるサービスがオススメです。有名なところだとGLSL Sandbox GalleryShadertoy BETAですね。
 
 今回はGLSL Sandbox Galleryを使った、リアルタイムアニメーションCG(デモシーン)の作成の流れを紹介しようと思います。
 この記事で皆さんが手軽にGLSLを触るようになり、レイマーチングをはじめとするリアルタイムレンダリングに興味を持っていただければ幸いです。

GLSL Sandboxの基本

 このサイトは、ピクセルシェーダーをリアルタイムにコーディングできるサイトです。こちら使い方は非常にシンプルで、GLSLを記述するとリアルタイムに変更が背景に反映されます。基本的な仕様は以下となります。
 
 ・gl_FragCoordに現在処理の対象となっているピクセルの座標が二次元ベクトルとして定義されます。 (定義域:(0, 0) ~ (xの解像度、yの解像度))
 ・gl_FragColorにそのピクセルの色をRGBAの4次元ベクトルで設定します。(値域:(0, 0, 0, 0) ~ (1, 1, 1, 1))

 試しに、gl_FragColorにRGB成分が全て0.5のベクトルを代入してみましょう。

// floatの精度を指定します
precision mediump float;

uniform float time;
uniform vec2 resolution;
uniform vec2 mouse;

void main( void ) {
	gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
}

 GLSL Sandboxでは3つのuniform変数が使えます。timeは経過時間resolutionはウィンドウの解像度、mouseは現在マウスがフォーカスしている座標です。mouse、resolutionはgl_FragCoordと同じ定義域を持ちます。

 このコードでは座標(gl_FragCoord)に関わらず一様に(0.5, 0.5, 0.5)の色を代入しているので、画面は全て灰色に染まることでしょう。また、数字を変化させるとリアルタイムに背景の色が変わって行くのがわかると思います。

 次に、座標を色で表してみます。ここで、座標(gl_FragCoord)は(0, 0)からresolution.xyの値をとりますが、色は0から1で指定しなくてはならないので、座標を0 ~ 1で正規化しましょう。

// 省略

void main( void ) {
        vec2 p = gl_FragCoord.xy / max(resolution.x, resolution.y);
	gl_FragColor = vec4(p, 0.0, 1.0);
}

 main関数を上記のように書き換えると、こんな画面が描画されることと思います。

f:id:riyaaaaasan:20181122142018p:plain

 R, G成分をX, Y座標に対応させているので、右に行けば行くほど赤く、上に行けば行くほど緑に近づいていきます。(縦横比が違う場合、厳密には短い方は0~1で正規化はできていません)

 GLSL Sandboxの基本的な解説は済んだので、本題のレイマーチングをこのGLSL Sandboxの上で実装してみましょう。

レイマーチングの基本

 レイマーチングとは、広義のレイトレーシングの一つです。カメラからレイを飛ばし、色を決定するところまでは同じですが、レイマーチングではレイの交差判定に距離関数というものを使います。距離関数とは、ある集合の中の二点の距離を定義する関数です。もっとも有名なのはユークリッド距離関数で、一般的な人間の感覚における「距離」を示す関数です。ユークリッド距離関数は距離関数の一つですが、ユークリッド距離関数 ≠ 距離関数であることには注意しましょう。

 つまり、レイマーチングは語弊を恐れずいうならば、全てのピクセルについて、シーン上に存在するオブジェクトを示す距離関数のどれかが0を返すまでレイを飛ばし(もしくは定められた計算数まで)、ヒットしたオブジェクトを描画する手法です。

f:id:riyaaaaasan:20181122160329p:plain

 実際に書いてみましょう。

// 球の距離函数
float sphere_d(vec3 p) {
	const vec3 sphere_pos = vec3(0.0, 0.0, 3.0);
	const float r = 1.0;
	return length(p - sphere_pos) - r;
}

struct Ray {
	vec3 pos;
	vec3 dir;
};

void main( void ) {
   // 画面座標の正規化。中心が(0, 0)の方が都合がいいため-1 ~ 1で正規化する
	vec2 pos = (gl_FragCoord.xy * 2.0 - resolution) / max(resolution.x, resolution.y);
	
        // カメラの位置。中心から後方にあるイメージ
	vec3 camera_pos = vec3(0.0, 0.0, -4.0);
        // カメラの上方向の姿勢を定めるベクトル この場合水平
	vec3 camera_up = normalize(vec3(0.0, 1.0, 0.0));
        //  カメラの向いている方向 
	vec3 camera_dir = normalize(vec3(0.0, 0.0, 1.0));
        // camera_upとcamera_dirの外積から定まるカメラの横方向のベクトル 
	vec3 camera_side = normalize(cross(camera_up, camera_dir));
	
        // レイの位置、飛ぶ方向を定義する
	Ray ray;
	ray.pos = camera_pos;
	ray.dir = normalize(pos.x * camera_side + pos.y * camera_up + camera_dir);
	
	float t = 0.0, d;
        // レイを飛ばす (計算回数は最大64回まで)
	for (int i = 0; i < 64; i++) {
		d = sphere_d(ray.pos);
                // ヒットした
		if (d < 0.001) {
			break;
		}
                // 次のレイは最小距離d * ray.dirの分だけ進める(効率化)
		t += d;
		ray.pos = camera_pos + t * ray.dir;
	}

	if (d < 0.001) {
                // ヒットしていれば白
		gl_FragColor = vec4(1);	
	} else {
		gl_FragColor = vec4(0);	
	}
}


 スフィア一個を配置し、カメラを画面後方に設定しました。位置関係を図で表すと以下のような感じです。


f:id:riyaaaaasan:20181128155827p:plain


 そして、以下のように描画されます。
 

f:id:riyaaaaasan:20181122163232p:plain


 これで最も基本的なレイマーチング は実装できました。しかし、せっかく3Dでやっているのに平べったく見えてしまいます。
 次はライティングをしてみましょう。

レイマーチングにおけるライティング表現

 今回はディレクショナルライトによるライティングを行います。必要なのは、光源ベクトルと、ライティング対象の法線ベクトルです。
 一般的な3Dのレンダリングにおいては、法線ベクトルは頂点データから与えられますが、レイマーチング の場合は距離函数による衝突判定を行っているため、同じの手法は使えません。
 ここで、数学的なアプローチにより、「陰関数の勾配ベクトルは法線ベクトルとなる」という性質を使って、法線ベクトルを求めてみたいと思います。
 勾配ベクトルとは、各成分の偏微分を並べたベクトルを指します。陰関数とは \boldsymbol{R}(x, y, z....) = 0の形で表される関数のことを指し、距離函数も陰関数表示の一つです。

 つまり、距離函数を使って任意の座標の法線ベクトルを求めるためには
  \boldsymbol{t} = (\frac{∂f}{∂x}, \frac{∂f}{∂y}, \frac{∂f}{∂z})を求める必要があります。xの偏微分の定義は以下。

 \frac{∂f(x, y, z)}{∂x} := \lim_{h \to 0}\frac{∂f(x + h, y, z) - ∂f(x, y, z)}{h}
 y, zの偏微分についても同様のことが言えます。

 数値計算的に求めてみましょう。

vec3 sphere_normal(vec3 pos) {
  float delta = 0.001;
  return normalize(vec3(
    sphere_d(pos + vec3(delta, 0.0, 0.0)) - sphere_d(pos),
    sphere_d(pos + vec3(0.0, delta, 0.0)) - sphere_d(pos),
    sphere_d(pos + vec3(0.0, 0.0, delta)) - sphere_d(pos)
  ));
}

 これで法線ベクトルを求めることができます。
 次に光源ベクトルをLとして、頂点の輝度を求めましょう。ここではシンプルにランバート反射を使います。

vec3 L = vec3(0.0, 0.0, 1.0); // 光源ベクトル
vec3 N = sphere_normal(ray.pos); // 法線ベクトル
vec3 LColor = vec3(1.0, 1.0, 1.0); // 光の色
vec3 I = dot(N, L) * LColor; // 輝度

 これらを先ほどの実装に組み込むと、このようにライティングされます。

f:id:riyaaaaasan:20181128164303p:plain

 ソースコードが長くなってしまったので、フルバージョンはgistにアップしています。
raymarching-sphere-lighting · GitHub

 光源ベクトルが(0.0, 0.0, 1.0)なので、球の正面に光を当てていることになりますね。光の方向を変えてみると光の当たり方が変わります。(必ずその際は光源ベクトルを正規化をしてください)

 他にも、距離函数にmod関数(剰余)をかますことで、レイの値域を制限し、あたかもオブジェクトが複製されているように見せかけるテクニックがあります。

float sphere_d(vec3 p) {
	const float r = 1.0;
	return length(mod(p, 4.0) - 2.0) - r;
}

 f:id:riyaaaaasan:20181128174155p:plain

 さて、ここまで基本を抑えてしまえばあとはなんでもありです。
 超かっこいいエフェクト(ブルームや走査線など)を好き放題追加するのもありですし、面白い距離函数を実装してみるのもいいかもしれません。

 最後に、パーリンノイズを使ったレイマーチング によるテライン描画をやってみましょう。

パーリンノイズを使ったレイマーチングによるテライン表現

 パーリンノイズは、テクスチャの表現によく使われる、とても自然に感じる(?)擬似乱数を生成するアルゴリズムです。話すと長くなるのでここでは端折って、解説や実装を引用したいと思います。

The Book of Shaders: Noise

 今回はこの中にあるパーリンノイズをさらに改良したシンプレックスノイズというのを使いましょう。このシンプレックスノイズを、距離函数として応用してレイマーチングに使用します。

 試しにまず、このシンプレックスノイズをテクスチャとして描画してみましょう。

void main( void ) {
	vec2 pos = (gl_FragCoord.xy * 2.0 - resolution) / max(resolution.x, resolution.y);
        // snoiseは上記文献で示されているシンプレックスノイズ関数
	float c = snoise(pos * 5.0);
        gl_FragColor = vec4(c, c, c, 1.0);	
}

f:id:riyaaaaasan:20181128175015p:plain

 この時、シンプレックスノイズ関数は、 y = f(x, z)という二変数関数で示される曲面であると解釈できます。(数学でよく表す空間と、GLSLの空間ではzとyが逆転していることに注意してください)
 つまり、レイの座標を(Rx, Ry, Rz)とすると、 f(Rx, Rz) - Ry = 0 を満たすとき、レイはこのシンプレックスノイズ曲面上にある(衝突した)と考えることができます!

 

float map(vec3 v) {
        // 地面のオフセット
        const float GROUND_BASE = 1.2;
	return v.y - snoise(v.xz * .4) + GROUND_BASE;
}

vec3 map_normal(vec3 v) {
	float delta = 0.01;
	return normalize(vec3(map(v + vec3(delta, 0.0, 0.0)) - map(v), 
			      map(v + vec3(0.0, delta, 0.0)) - map(v), 
			      map(v + vec3(0.0, 0.0, delta)) - map(v)));
}

 謎のマジックナンバーが出てきていますが、これは自由に変えることが可能なパラメータです。試しにいじってみるのもいいでしょう。

 さて、このmap関数をスフィアの距離函数の代わりに使い描画してみると...
 raymarching-terrain-sample · GitHub


 f:id:riyaaaaasan:20181128180017p:plain

 見事テラインが描画されました! 今回は、カメラの座標にtimeを加えているので、リアルタイムにテラインが動いていくのがわかると思います。

おわりに

 レイマーチングは解説した通りレイトレーシングの交差判定に距離函数を使うだけのシンプルなアルゴリズムです。一方で、その応用範囲は無限大です。距離函数はその特性上、幾何的なオブジェクトの描画に向いていますが、組み合わせや微調整を繰り返すことで、生き物のようなオブジェクトを描画することだって可能です。ボリュームレンダリングという3次元的な広がりのあるデータの描画に応用して雲などを描画することもできます(こちらは、実際に多くのゲームへの採用例があります)。
 他にも、パーリンノイズによるテライン表現を応用すると、グランドキャニオンのような光景や、リアルな水面でさえも描画することができます。
 このように、レイマーチング等を用いたデモシーンは、様々な工夫や自分の考えた最強の関数などを好き放題盛り込んで一つの作品を作る面白い分野です。ぜひ遊んで、あなただけの作品を作ってみてください。