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

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

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次元的な広がりのあるデータの描画に応用して雲などを描画することもできます(こちらは、実際に多くのゲームへの採用例があります)。
 他にも、パーリンノイズによるテライン表現を応用すると、グランドキャニオンのような光景や、リアルな水面でさえも描画することができます。
 このように、レイマーチング等を用いたデモシーンは、様々な工夫や自分の考えた最強の関数などを好き放題盛り込んで一つの作品を作る面白い分野です。ぜひ遊んで、あなただけの作品を作ってみてください。

 
 
 

Akatsuki Game JAM 開催レポート

こんにちは、エンジニア採用担当の花田です。

去る9月8日(土)〜9日(日)に2020年新卒エンジニア向けインターンシップ「Akatsuki Game JAM」を開催いたしました。今年で第4回となるその様子を「Server Sonic 2018(通称:サバソニ)」に続き、紹介させていただきます。

「Akatsuki Game JAM」とは・・・

2日間を通して、ゲームアイデアの企画から開発までを行い、その制作物の結果を競い合うという内容です。今回は、アカツキで大切にしている「つながり」「Shine(輝く)」という2つの言葉をテーマにUnityでのチーム開発を行います。参加者は17人、5チームです。

 ▼詳細はこちら

https://aktsk.jp/recruit/new_recruit2020/gamejam2018/

#なぜこのようなインターンシップアカツキが実施するのか

私たちは本気で「ゲーム開発を通して、世の中をワクワクさせるようなプロダクトづくりを体験をしてもらいたい!」という想いを持っているからです。

もちろん、参加者の中にはこれまでにもハッカソン参加の経験や、サークルなどでのチーム開発を経験している参加者も多くいました。一方で、多くの学生とお話をする中で、プロのエンジニアから近い距離でアドバイスをもらいながら、開発を進める経験をしたことがある学生は少なく、その機会を創出したいと思い開催しています。今年で4回目になりますが、毎年パワーアップしております。

#昨年から何を変えたのか

このインターンシップでは、アカツキの組織文化も一緒に体現できるインターンシップにしたいと考えました。そのため、従来の1日開催から2日間に期間を拡大し、更にアカツキのエンジニアが各チームの専任メンターとしてつくことで、より濃密なコミュニケーションが取れる様に変更を加えました。
併せて期間拡大により時間的余裕が生まれたことで、ワクワクするアイデアを生み出すためのアイデアソンの充実化や、オフィス見学、各チームの制作物の試遊会、アカツキの振り返りの文化も体験してもらう事ができました。

#当日の様子

春から多くの学生との面談や面接を実施して、いよいよ開催当日を迎えます。

インターンシップでも恒例となった「Good & New」(24時間以内にあった「よかった事」「新しい気づき」の共有)でアイスブレイクをし、早速開発ゲームのアイデアソンへと移ります。

▼Good & New

f:id:kenji-hanada:20180912163252j:plain▼アイデアソンの説明

f:id:kenji-hanada:20180912163255j:plain今回この段階では、アイデアは全参加者のものという考えのもと、全チームのゲーム企画を共有し、発想を拡げた後に、各チームがブラッシュアップするという方法をとりました。チーム戦ではあるものの、より多くのアイデアに触れることで、企画が磨かれていくのを感じました。

▼アイデアを張り出し、全チームに公開

f:id:kenji-hanada:20180912163259j:plainゲーム企画が決まったところで、いよいよ開発開始です。2日間の開発スケジュールや役割分担を決め、本番が始まるこの瞬間に、会場の雰囲気も一層熱を帯びた様に感じました。今回、各チームを専任メンターが担当し、参加者からの質問対応や、適宜アドバイスと、交流も積極的に。VRゲームを開発するチームに対し、Oculus Goを貸与した為、初めて触るメンバーは環境構築に手こずりながらも、奮闘していました。

▼いよいよ開発開始、メンターからのアドバイス

f:id:kenji-hanada:20180912163302j:plain2日間に拡大したものの、開発時間はあっという間に過ぎ、成果物のプレゼンテーションへ。

各チーム、そのゲームのこだわりポイントや面白み、また当初想定していた機能が時間が足りずに実装に至らなかった悔しさを口にしながらも、全力で取り組んだこの2日間をアピールしました。

▼プレゼンテーションの様子

f:id:kenji-hanada:20180912163312j:plain発表に対しては、アカツキメンバーと参加者から様々な質問が飛びました。質問は「実装にはどんな技術を使用したのか」「盛り込めなかった機能が実装されれば、どのように面白みが拡がるか」「コンセプトが尖っているが、どのような発想から生まれたのか」など、時に会場は笑いにつつまれながら、発表を終えました。
アカツキメンバーからの質問タイム

f:id:kenji-hanada:20180912163316j:plain発表後には、各チームのゲームを体験できる試遊会を実施。自分たちのゲームを他チームに説明、体験する側ものめり込んでプレイする姿が印象的でした。2日間に拡大したことで、参加者が全力を注ぎ込んだプロダクトに触れる時間を設けることができ、お互いの健闘を称える良い時間となりました。

▼試遊会

f:id:kenji-hanada:20180912163306j:plainそして、いよいよ緊張の結果発表へ。

まずは準優勝チームを表彰。「地下アイドルの守護神」というキャッチーなタイトルとアイドルに襲いかかる悪質なファンをサイリウムを投げて撃退するというコンセプトに会場は興味津々でした。このゲームはOculus Goを使用して、コントローラーを振りかざすとサイリウムを投げられるという仕組み。時間の都合上、一部実装が間に合わない部分があり、悔しさを見せていましたが、堂々の準優勝。結果発表の際には喜びの息もぴったりで、チームワークの良さが表れていました。

▼喜ぶ準優勝チーム

f:id:kenji-hanada:20180912163319j:plainそして、栄えある優勝は「Doki☆Doki☆Dungeon」というダンジョン探索ゲームを開発したチームでした!暗闇のダンジョンにそれぞれ特性を持った3色のライトを配置することで、迫り来る敵を避けながら宝を集めるという内容です。2日間でゲームとして仕上げたこと、Joy-Conを使用することで、振動によって敵や宝との距離を察知する機能まで実装し、見事優勝に選ばれました!

▼優勝チーム

f:id:kenji-hanada:20180912163322j:plain#最後に、運営側としての感想

2日間という非常に短期間ではありましたが、世の中をワクワクさせる様なゲーム開発に全力で取り組むみなさんのキラキラした姿を見れて感無量でした。中には今回開発したゲーム開発を継続したいという声もあり、非常に嬉しいです。是非、今回のインターンシップが一つの学びとなり、みなさんの次なる挑戦に繋がれば幸いです!

▼参加者のみなさん、本当にお疲れ様でした!

f:id:kenji-hanada:20180912163329j:plain

Server Sonic 2018 開催レポート

こんにちは、エンジニア採用担当の花田です。
この度8月25日(土)〜26日(日)に今年で第2回目の開催となる新卒エンジニア向けインターンシップ「Server Sonic 2018(通称:サバソニ)」を開催いたしました。その様子を紹介させていただきます。

「Server Sonic 2018(通称:サバソニ)」とは・・・

2日間を通して、ゲームサーバーアプリケーションの性能改善方法を学び、Akatsuki x KADOKAWAが贈る青春体験型野球ゲーム「八月のシンデレラナイン」(ハチナイ)の性能改善に2人1組で取り組み、結果を競い合うという内容です。

▼詳細はこちら https://aktsk.jp/recruit/new_recruit2020/serversonic2018/

#なぜこのようなインターンシップアカツキが実施するのか
一言で言うと「ワクワクする体験を通して、学びを得てもらいたい!」という気持ちからです。

エンジニア職を目指す学生とお会いする中で、多くの学生から「個人やサークル規模では体験できないインターンシップに参加したい」という声を聞いていました。

そこで、社内のエンジニアチームとどうすれば、「個人やサークル規模では体験できないインターンシップ」を開催できるかを話し合い、実際にリリースされている大規模プロダクトのソースコードに触れることができるインターンシップを企画しました。昨年も同様の思いから、第1回を実施しましたが「難易度が高い」「単なる高速化ではなく、プロダクトとして良質なコードを書きたい」という意見から、1日開催から2日間の開催へと変更し、講義パートの比重を大きく変更しコンテスト自体にゲーム性を加える事で、参加者がワクワクし、のめりこめるような内容を企画いたしました。

数ヶ月に渡る準備期間を経て、 いよいよ開催当日。

▼会場入口

f:id:kenji-hanada:20180829195000p:plain
会場の入口にはハチナイメンバーが勢揃いし、参加者を迎え入れる

まずは、アカツキの文化として根付いている「Good & New」(24時間以内にあった「よかった事」「新しい気づき」の共有)でアイスブレイク。他社のインターンなどで顔を合わせていたり、まさかの同じサークルに所属しているメンバーもいたりと、朝から話が弾み、笑いが生まれる和やかな雰囲気でスタート。

▼Good & New

f:id:kenji-hanada:20180829195005p:plain

その後、アカツキメンバーから性能改善に関する講義を入門編&応用編と二部構成にて実施。特に応用編は難易度の高い内容でしたが、参加者からも質問が飛び、積極的に吸収し、午後の開発に向け知識を蓄えている姿が印象的でした。

▼性能改善に関する講義

f:id:kenji-hanada:20180829195009p:plain

講義を終えたところで、いよいよ開発開始。

各チーム、それぞれの得意領域から役割分担をし、開発を進めます。ゲーム構成のどの部分を改善すると結果が得られそうか、仮説を立てながら取り組んでいました。

▼いよいよ開発開始

f:id:kenji-hanada:20180829195014p:plain

 開発中にはアカツキのメンターが、参加者の質問に答え、開発をサポート。Slack上での質問や、画面を眺めながらの対応と、2日間を通し活発なコミュニケーションを取っていました。

 ▼参加者の質問にはメンターがサポート

f:id:kenji-hanada:20180829195019p:plain
また今回、CI(継続的インテグレーション)を回すことで、改善結果がポイントとして表示される仕組みを構築したため、参加者はテスト実施の度に、ワクワクしたり、時には思う様に結果が得られず落ち込んだりと開発そのものに含まれたゲーム性を楽しんでいました!

 ▼テストを試す度にポイントを表示

f:id:kenji-hanada:20180829195024p:plain

このポイントは、途中経過としてチーム名を伏せて、参加者に公開し、自分たちが今、何番手に位置しているのかということを把握することで、終盤は熱いデッドヒートが繰り広げられました。

あっという間に開発時間が過ぎ、いよいよ結果発表へ!緊張高まる中、順位が発表され、優勝チームは両手を高らかにハイタッチをして喜びを噛み締めていました^^

今回、準優勝チームにはアカツキオリジナルパーカーを、優勝チームにはSwitch、PS4 Pro、Kinesisなどの豪華賞品から希望のものを贈りました。

 ▼優勝・準優勝チーム

f:id:kenji-hanada:20180829195027p:plain

その後、CTOの田中から、順位に限らず参加者からの優れたコミットの発表(優秀コード部門、ネタ部門など)があり、そのコードを書いた参加者が意図を話して、2日間を振り返りました。

▼CTO田中からの優れたコミットを発表

f:id:kenji-hanada:20180829195031p:plain

インターンシップの締めくくりは、懇親会。アカツキの子会社であるアカツキライブエンターテイメントが提供するgoodyというケータリングを囲み、参加者とメンターが和気藹々と交流しました。

▼懇親会の様子

f:id:kenji-hanada:20180829195035p:plain

#最後に、運営側としての感想

至らぬ点もあったかと思いますが「ワクワクする体験を通して、学びを得てもらいたい」という思いは幸運にも参加者のみんなに伝わったと感じています!結果を残せて喜んでいるメンバーや、自分の力不足に悔しさを見せるメンバー、それぞれの表情を見ながら、みなさんの今後の成長と活躍がとても楽しみになりました。参加者のみなさん、今回の振り返りを経て得た学びを、是非次なるステージに繋げていただければと思います^^

▼参加者のみなさん、本当にお疲れ様でした!

f:id:kenji-hanada:20180829195040p:plain