BioErrorLog Tech Blog

試行錯誤の記録

Ubuntuの鶴の壁紙の詳細を調べる

Ubuntuに入っている綺麗な鶴の壁紙の詳細を追います。

はじめに

Ubuntuの鶴の壁紙

このUbuntuの鶴の壁紙、綺麗で面白い、いい雰囲気ですね。

詳細が気になったので調べてまとめます。


※ 以前調べたときの下記メモをもとに再整理したものです。

Ubuntuの鶴の壁紙の詳細を調べる

画像ファイルを調べる

Ubuntu上に保存されている壁紙画像ファイルを見てみます。

調べてみると、デフォルト壁紙ファイルたちは /usr/share/backgrounds に配置されているとのこと。

そこを見てみると確かに壁紙ファイルが配置されています。

Ubuntuのデフォルト壁紙ファイル

少し眺めてみると、多くのファイル名は<壁紙名>_by_<作者名>のフォーマットで記載されていることがわかります。

しかし、例の鶴の壁紙のファイル名はhardy_wallpaper_uhd.png

作者名も記載されてないし、意味もいまいち分かりません。

この絵が作られた場所

ファイル名から直接的な情報は得られませんでしたが、このファイル名を頼りに検索をかけてみます。

少し探してみると、Ubuntuのwikiページがヒットしました。

Artwork/Incoming/Hardy/Alternate/Fela_Kuti - Ubuntu Wiki

まさにここで、この絵の原型が提案され、議論され、コミュニティの中で改善が提案されていく様子が残っています。

誰かが一人で完結して描いたものではなく、コミュニティー全体で作られていたんですね。 まさにLinuxの思想という感じがします。

壁紙について議論・改善案の提案がコミュニティーで繰り返される様子

この絵のタイトル

この絵のタイトルは、

Fela Kuti
A Fela Kuti inspired heron.

だそうです。

"サギをイメージしたFela Kuti"

といったところでしょうか。

この鳥、鶴ではなくサギだったみたいですね。
(そもそもこの形状の鳥を見て鶴を想起するのは日本人だけか)

"Fela Kuti"の方は、検索してみるとミュージシャンばかりがヒットします。 この絵とミュージシャンFela Kutiが関係あるのか、はたまた全然関係ないのか、ちょっとわかりませんでした。

ミュージシャン"Fela Kuti" | 画像はWikipediaより引用

元データで遊ぶ

下記のページには各バージョンのソースデータが置かれているので、少し触ってみます。

attachment:source.svg of Artwork/Incoming/Hardy/Alternate/Fela_Kuti - Ubuntu Wiki

これもなかなか面白いです。

背景の模様も面白いなあと思って見てましたが、これらは拡大したサギ自身でした。

壁紙のソースデータ. 背景の模様も拡大したサギ自身だった


なお、svgファイルで配られているので、自分でも簡単に色や配置を変えることができます。

最後に自分でいじってみたバージョンを貼って、この調査を終わろうと思います。

ソースデータをいじって自分でアレンジしてみたver

おわりに

以上、Ubuntuの鶴(サギ)の壁紙の詳細を追いかけました。

同じく気になった方がもしいれば、参考にしていただけると幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Artwork/Incoming/Hardy/Alternate/Fela_Kuti - Ubuntu Wiki

attachment:source.svg of Artwork/Incoming/Hardy/Alternate/Fela_Kuti - Ubuntu Wiki

Where are the Unity desktop wallpapers located? - Ask Ubuntu

Fela Kuti - Wikipedia

Go製ゲームエンジンEbitengine入門: Boids Flockingを実装する

Boids Flockingの実装を通して、Go製のゲームエンジンEbitengineに入門します。

はじめに

Go製のゲームエンジンEbitengineというものを見つけて面白そうだったので、取り急ぎ何か作ってみようと思いました。

以前に他のゲームエンジンで実装したことのあるBoids Flockingを実装し、Ebitengineに入門していきたいと思います。

# 作業version
Go: 1.20.7
Ebitengine: v2.5.6 

Ebitengineとは

Ebitengineは、

  • Go製のオープンソース2Dゲームエンジン
  • マルチプラットフォーム対応 (Nintendo Switchもサポート)
  • ミニマムなAPI
  • 「全ては矩形画像である」- 矩形画像から矩形画像へ描画でゲームを構成する

という特徴を持ったゲームエンジンです。

ebitengine.org

設計思想はEbitengine作者様の記事にわかりやすく解説されていますので、ご興味があれば一読をお勧めします。

Ebitenginで作られた有名どころのゲーム事例としては、

などがあります。

どちらもOdencatさんからのゲーム(Nintendo Switchにも移植されてる)なのですが、OdencatさんがEbitengineを採用するに至った経緯や、このツールの活用方法を語った下記の資料も面白いのでおすすめです。

www.slideshare.net

今回作ったもの: Boids Flocking

https://github.com/bioerrorlog/boids-ebitengine/blob/main/screenshots/demo.gif?raw=true

Boids Flockingは、鳥の群れの動きをシミュレートする人工生命モデルです。 "Bird-oid"「鳥っぽいもの」という意味で、Boidsと呼ばれています。

Boids Flockingのアルゴリズム自体の説明については過去記事で整理しましたので、そちらを参照ください。

最終的なコードはGitHubに配置していますので、以降はこのコードを元にしながらポイントを整理していきます: github.com

Boids Flockingを実装する

最小構成: Hello, World!

Boids Flockingの実装に移る前に、まずはEbitengine製ゲームにおける最小構成を確認しておきます。

// main.go
package main

import (
    "log"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

type Game struct{}

func (g *Game) Update() error {
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    ebitenutil.DebugPrint(screen, "Hello, World!")
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return 320, 240
}

func main() {
    ebiten.SetWindowSize(640, 480)
    ebiten.SetWindowTitle("Hello, World!")
    if err := ebiten.RunGame(&Game{}); err != nil {
        log.Fatal(err)
    }
}

Game structを用意し、3つの関数Update Draw Layoutを実装して上記のようにRunGameに渡してあげれば、ゲームを実行することができます。

  • Updateはフレームごとに呼び出される処理で、主にゲームロジックの実行/ゲームステートの更新を担わせます。
  • Drawもフレームごとに呼び出される処理で、こちらは描画処理を担います。
  • Layoutはゲームのスクリーンサイズを定義します。

go run main.goでゲームを実行できます。

Ebitengineで Hello, World! を表示

Boids Flockingを実装する時も同じく、ロジックをUpdateに実装し、Drawで描画処理を実装する、という形をとっていきます。

ディレクトリ構成

Boids Flockingを実装するにあたって、main.goに全コードを書くと流石に読みにくくなってしまうので、下記のようなディレクトリ構成にしてみました。

# treeコマンド結果から抜粋
.
├── go.mod
├── go.sum
├── main.go
├── boids
│   ├── boid.go
│   └── game.go
└── vector
    ├── vec2.go
    └── vec2_test.go

エントリーポイントとなるmain.go、ゲームロジックを実装するboidsパッケージ、あとは二次元ベクトル操作処理を実装するvectorパッケージです。 ベクトル処理は既存のライブラリを使った方が良いのでしょうが、今回は諸々の入門として自分で書いています。

main.goは、boidsパッケージで定義するgameロジックを呼び出すだけにとどめています。

package main

import (
    "log"

    "github.com/bioerrorlog/boids-ebitengine/boids"
    "github.com/hajimehoshi/ebiten/v2"
)

func main() {
    game, err := boids.NewGame()
    if err != nil {
        log.Fatal(err)
    }
    ebiten.SetWindowSize(boids.ScreenWidth, boids.ScreenHeight)
    ebiten.SetFullscreen(true)
    ebiten.SetWindowTitle("Boids")
    if err := ebiten.RunGame(game); err != nil {
        log.Fatal(err)
    }
}

Ref. GitHub - bioerrorlog/boids-ebitengine at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84

ゲームロジックの実装

こちらが大本となるゲームロジックです。

package boids

import (
    "fmt"
    "image/color"
    "math/rand"

    "github.com/bioerrorlog/boids-ebitengine/vector"
    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

const (
    ScreenWidth  = 1920
    ScreenHeight = 1080
    boidCount    = 100
)

type Game struct {
    boids []*Boid
}

func NewGame() (*Game, error) {
    g := &Game{
        boids: make([]*Boid, boidCount),
    }

    for i := range g.boids {
        g.boids[i] = NewBoid(
            rand.Float64()*ScreenWidth,
            rand.Float64()*ScreenHeight,
            vector.Vec2{X: ScreenWidth / 2, Y: ScreenHeight / 2},
        )
    }
    return g, nil
}

func (g *Game) Update() error {
    for _, b := range g.boids {
        b.Update(g.boids)
    }
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    // Backgroud
    screen.Fill(color.RGBA{255, 245, 228, 0xff})

    // Boids
    for _, b := range g.boids {
        b.Draw(screen)
    }

    // Debug
    fps := fmt.Sprintf("FPS: %0.2f", ebiten.ActualFPS())
    ebitenutil.DebugPrint(screen, fps)
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return ScreenWidth, ScreenHeight
}

Ref. boids-ebitengine/boids/game.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

NewGame関数からGame structを外部から取得できるようにしています。

Game structは後述するBoidを配列で保持しており、UpdateではBoidのUpdate関数を呼び出します。

Drawでは描写を行っており、順番に背景描画、Boidsの描画、デバッグ(fps表示)描画を行っています。

Boids Flockingロジックの実装

次はGameから呼び出しているBoid個々のロジックの実装です。

package boids

import (
    "image/color"
    "math/rand"

    "github.com/bioerrorlog/boids-ebitengine/vector"
    "github.com/hajimehoshi/ebiten/v2"
    ev "github.com/hajimehoshi/ebiten/v2/vector"
)

const (
    moveSpeed                 = 20
    perceptionRadius          = 100
    steerForce                = 1
    alignmentForce            = 0.1
    cohesionForce             = 0.05
    separationForce           = 0.3
    centralizationForce       = 0.3
    centralizationForceRadius = 200
)

type Boid struct {
    position, velocity, targetCenter vector.Vec2
}

func NewBoid(x, y float64, targetCenter vector.Vec2) *Boid {
    return &Boid{
        position:     vector.Vec2{X: x, Y: y},
        velocity:     vector.Vec2{X: rand.Float64()*2 - 1, Y: rand.Float64()*2 - 1},
        targetCenter: targetCenter,
    }
}

func (b *Boid) Draw(screen *ebiten.Image) {
    ev.DrawFilledCircle(screen, float32(b.position.X), float32(b.position.Y), 20, color.RGBA{255, 148, 148, 0xff}, true)
}

func (b *Boid) Update(boids []*Boid) {
    neighbors := b.getNeighbors(boids)

    alignment := b.alignment(neighbors)
    cohesion := b.cohesion(neighbors)
    separation := b.separation(neighbors)
    centering := b.centralization()

    b.velocity = b.velocity.Add(alignment).Add(cohesion).Add(separation).Add(centering).Limit(moveSpeed)
    b.position = b.position.Add(b.velocity)
}

// 以下略

Ref. boids-ebitengine/boids/boid.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

NewBoidで初期化されたBoidを返し、Drawで描画、UpdateでBoidsアルゴリズムを実行しています。

Boidsアルゴリズムとして、

  • alignment: 近隣のboidsと同じ方向に進もうとする力
  • cohesion: 近隣のboids集合の中心地に向かおうとする力
  • separation: 近隣のboidsと近づきすぎないよう距離をとる力

の3つを掛け合わせています。

それとは別に、boids達が画面外に行ってしまわないよう画面中心へと向かう力centralizationも追加しています。

それぞれの力についての詳しい実装はここでは取り上げない(上記コード// 以下略部分)ので、ご興味ある方はソースコードをご覧ください。

あとはconstで定めた各パラメータの微修正を繰り返し、なんとなく滑らかでいい感じになるようにしました。 出来上がったBoidsたちの動きは、なかなか気に入っています。

(再掲) https://github.com/bioerrorlog/boids-ebitengine/blob/main/screenshots/demo.gif?raw=true

Vector処理の実装

上記Boidの実装では、自前のvector処理を利用しています。 (本来はライブラリを使った方が良いと思いますが、せっかくの入門なので自分で書いています)

package vector

import "math"

type Vec2 struct {
    X, Y float64
}

func (v Vec2) Add(other Vec2) Vec2 {
    return Vec2{v.X + other.X, v.Y + other.Y}
}

func (v Vec2) Sub(other Vec2) Vec2 {
    return Vec2{v.X - other.X, v.Y - other.Y}
}

func (v Vec2) Mul(scalar float64) Vec2 {
    return Vec2{v.X * scalar, v.Y * scalar}
}

func (v Vec2) Div(scalar float64) Vec2 {
    return Vec2{v.X / scalar, v.Y / scalar}
}

func (v Vec2) Length() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vec2) Normalize() Vec2 {
    length := v.Length()
    if length != 0 {
        return Vec2{v.X / length, v.Y / length}
    }
    return Vec2{0, 0}
}

func (v Vec2) Limit(max float64) Vec2 {
    if v.Length() > max {
        return v.Normalize().Mul(max)
    }
    return v
}

func (v Vec2) DistanceTo(other Vec2) float64 {
    diff := v.Sub(other)
    return diff.Length()
}

Ref. boids-ebitengine/vector/vec2.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

Boidsアルゴリズムで使うことになる、二次元ベクトル操作の実装です。

ゲームロジックや描画の実装にはなかなかテストを書きにくいこともありますが、このような純粋なGoコードであれば普通にテストが書けます:

package vector

import (
    "math"
    "testing"
)

func TestVec2_Add(t *testing.T) {
    tests := []struct {
        name  string
        v     Vec2
        other Vec2
        want  Vec2
    }{
        {"basic add", Vec2{3, 4}, Vec2{1, 2}, Vec2{4, 6}},
        {"add with zero", Vec2{3, 4}, Vec2{0, 0}, Vec2{3, 4}},
        {"add with negative values", Vec2{3, 4}, Vec2{-1, -2}, Vec2{2, 2}},
        {"add with same values", Vec2{3, 4}, Vec2{3, 4}, Vec2{6, 8}},
        {"add with floating values", Vec2{3.5, 4.5}, Vec2{1.5, 2.5}, Vec2{5, 7}},
        {"add with mix of positive and negative", Vec2{3, -4}, Vec2{-1, 2}, Vec2{2, -2}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := tt.v.Add(tt.other); got != tt.want {
                t.Errorf("Vec2.Add() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestVec2_Sub(t *testing.T) {
    tests := []struct {
        name  string
        v     Vec2
        other Vec2
        want  Vec2
    }{
        {"basic subtraction", Vec2{3, 4}, Vec2{1, 2}, Vec2{2, 2}},
        {"subtract with zero", Vec2{3, 4}, Vec2{0, 0}, Vec2{3, 4}},
        {"subtract negative values", Vec2{3, 4}, Vec2{-1, -2}, Vec2{4, 6}},
        {"subtract same values", Vec2{3, 4}, Vec2{3, 4}, Vec2{0, 0}},
        {"subtract floating values", Vec2{3.5, 4.5}, Vec2{1.5, 2.5}, Vec2{2, 2}},
        {"subtract mix of positive and negative", Vec2{3, -4}, Vec2{-1, 2}, Vec2{4, -6}},
        {"subtract resulting in negative", Vec2{3, 4}, Vec2{5, 6}, Vec2{-2, -2}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := tt.v.Sub(tt.other); got != tt.want {
                t.Errorf("Vec2.Sub() = %v, want %v", got, tt.want)
            }
        })
    }
}

// 以下略

Ref. boids-ebitengine/vector/vec2_test.go at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

テストを実行し、ALL PASSすれば安心です。

$ go test -v ./...
?       github.com/bioerrorlog/boids-ebitengine [no test files]
?       github.com/bioerrorlog/boids-ebitengine/boids   [no test files]
=== RUN   TestVec2_Add
=== RUN   TestVec2_Add/basic_add
=== RUN   TestVec2_Add/add_with_zero
=== RUN   TestVec2_Add/add_with_negative_values
=== RUN   TestVec2_Add/add_with_same_values
=== RUN   TestVec2_Add/add_with_floating_values
=== RUN   TestVec2_Add/add_with_mix_of_positive_and_negative
--- PASS: TestVec2_Add (0.00s)
    --- PASS: TestVec2_Add/basic_add (0.00s)
    --- PASS: TestVec2_Add/add_with_zero (0.00s)
    --- PASS: TestVec2_Add/add_with_negative_values (0.00s)
    --- PASS: TestVec2_Add/add_with_same_values (0.00s)
    --- PASS: TestVec2_Add/add_with_floating_values (0.00s)
    --- PASS: TestVec2_Add/add_with_mix_of_positive_and_negative (0.00s)

# 中略

PASS
ok      github.com/bioerrorlog/boids-ebitengine/vector  (cached)

ゲーム側の実装はこれで完成です。

CIの実装

最後にGitHub Actionsを設定して、コードの静的解析やbuild、testの実行を自動化させます。

name: Test

on:
  - push
  - pull_request

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Go
        uses: actions/setup-go@v3
        with:
          go-version: '1.20'

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev

      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v3
        with:
          version: latest

      - name: Vet
        run: go vet ./...

      - name: Build
        run: go build -v ./...

      - name: Test
        run: go test -v ./...

Ref. boids-ebitengine/.github/workflows/test.yml at aa86fe9fe9d4a8626ef1d652338a5dce1e7a5f84 · bioerrorlog/boids-ebitengine · GitHub

必要なライブラリをインストールしたのち、静的解析(golangci-lintgo vet)、go buildgo testを実行しています。

Install dependenciesではいろんなものをインストールさせていますが、これをしないとubuntuではgo vetが落ちるので注意です。 (インストールするライブラリは本家Ebitengineのworkflowを参考にしました)

おわりに

Go製のゲームエンジンEbitengineで、Boids Flockingを実装してみました。

個人的にはこれまで、

  • 自分でゲーム作るならシンプルな2Dゲーム一択 (2Dゲーム好きだし、3D酔いしやすいので)
  • そうなると、既存のゲームエンジンはぶっちゃけ機能が過剰
    • 何かにハマったときの切り分けがつらい (徐々にモチベに悪影響)
    • (もちろん使いこなせば高い生産性が出せるのかもしれないが...)

という思いを抱いてきたので、APIがシンプルなEbitengineは触っていて心地良かったです。

以上、ちょっとした一例としてどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - hajimehoshi/ebiten: Ebitengine - A dead simple 2D game engine for Go

Ebitengine - A dead simple 2D game engine for Go

ebiten package - github.com/hajimehoshi/ebiten/v2 - Go Packages

ゲームエンジンはアートである - 8 年以上自作ゲームエンジンをメンテし続けている話|Hajime Hoshi

既に拡張性のある無料のゲームエンジンがある中,なぜ時間と労力をかけて独自のゲームエンジンを開発していらっしゃるのでしょうか? | mond

Real-world game development with Ebitengine - How to make the best-selling Go game | PPT

GitHub - bioerrorlog/boids-ebitengine: Boids flocking simulation with Ebitengine

Generative ArtでNFTアート画像生成 | p5.js, Processing

GenerativeArtNFTで使ったアート画像生成ロジックをメモします。

はじめに

こんにちは、@bioerrorlogです。

先日、@hoosan16さん@ClankPanさんと参加したDfinityのハッカソンの一環として、Generative ArtのNFTを作成しました。

github.com

※ハッカソンで提出したメインプロダクト - Flippyがデモ用途で利用するNFT実装です。

本記事では、このGenerativeArtNFTのアート画像生成ロジックをまとめておきます。

Generative ArtでNFTアート画像生成

やりたいこと

TokenIdをinputとして無限で多様なGenerative Artを生成する、というのがやりたいことです。

アプローチとしては、TokenIdのhash値からパラメータを抽出し、Generative Artを生成するようにしました。 Generative Artの生成にはp5.jsを使っています。

アート模様はパーリンノイズをベースにして、カラーもhash値に応じて変わるようにする、というのが当初のアイデアです。

元バージョン: Reactコンポーネントとしてのプロトタイプ

もともとはフロントサイドで画像生成する想定だったので、Reactコンポーネントの形でプロトタイプを実装しました。

github.com

import React from 'react';
import p5 from 'p5';
import md5 from 'md5';

const sketch = (p: p5) => {
  const TokenId = 1 // pseudo TokenId

  const hashedNftSeed = md5(String(TokenId))
  console.log(hashedNftSeed)

  const getSlicedNum = (x: number, y: number) : number => {
    return parseInt(hashedNftSeed.slice(x, y), 16)
  }

  const lineLengthSeed = getSlicedNum(0, 2) + 1; // 1~257

  const strokeColorV1 = getSlicedNum(2, 4); // 0~255
  const strokeColorV2 = getSlicedNum(4, 6); // 0~255
  const strokeColorV3 = getSlicedNum(6, 8); // 0~255

  const noiseScale = (getSlicedNum(8, 11) % 1000) / 10000; //0.0001~0.1000

  const rotateSeed = getSlicedNum(11, 13); // 0~255

  const backgroundColorV1 = getSlicedNum(15, 17); // 0~255
  const backgroundColorV2 = getSlicedNum(17, 19); // 0~255
  const backgroundColorV3 = getSlicedNum(19, 21); // 0~255

  const drawIterateVal = getSlicedNum(21, 26) + 100000; // 100000~1000000

  const randomSeedVal = getSlicedNum(26, 29); // 0~4096
  const noiseSeedVal = getSlicedNum(29, 32); // 0~4096

  p.setup = () => {
    p.createCanvas(500, 500).parent('p5sketch');
    p.background(backgroundColorV1, backgroundColorV2, backgroundColorV3, 255);
  
    p.randomSeed(randomSeedVal);
    p.noiseSeed(noiseSeedVal);
   
    for (let i = 0; i < drawIterateVal; i++){
      const x = p.random(p.width);
      const y = p.random(p.height);
      const noiseFactor = p.noise(x*noiseScale, y*noiseScale);
      const lineLength = noiseFactor * lineLengthSeed;
      
      p.push();
      p.translate(x, y);
      p.rotate(noiseFactor * p.radians(rotateSeed));
      p.stroke(strokeColorV1, strokeColorV2, strokeColorV3, 3);
      p.strokeWeight(1);
      p.line(0, 0, lineLength, lineLength);
      p.pop();
    }
  }
}

// ref. https://discourse.processing.org/t/instance-mode-creating-two-canvas/14121
new p5(sketch);

const DemoNft: React.FC = () => {
  return (
    <div id = "p5sketch">
      {/* p5 instance will be created here --> */}
    </div>
  );
}

export default DemoNft;

TokenIdがハードコードされているのは、プロトタイプだったからということでご容赦を。。

TokenIdをhash化した上で、アート画像を生成するための各パラメータをhash値のsliceから割り当てる、という方法をとってみました。

発想はシンプルですが、それぞれのTokenIdから全く異なるテイストのアート画像を生成することに成功しています。

出力アート画像例:

入力TokenId, 左上から順に1~4, 5~8

入力TokenId, 左上から順に1000~1003, 1004~1007

入力TokenId, 左上から順に1000000001~1000000001, 1000000005~1000000005

最終バージョン: ts-nodeで実行

上記をベースとしつつも、修正した上で最終プロダクトに利用しています。

  1. Reactコンポーネントではなく、ts-nodeスクリプトとして実行できる形にする
  2. アート画像生成ロジック/出力結果を軽くする

フロントエンド側で都度アート画像生成をさせるのは処理が重すぎるので、あらかじめ生成したアート画像をCanisterにアップロードするよう設計を変更しました。

それでもまだ表示速度に問題があったので、生成画像サイズを軽くする変更を加えています。

最終的なソースコードはこちら: github.com

おわりに

以上、GenerativeArtNFTで使ったアート画像生成ロジックをメモしました。

久しぶりにGenerative Artなコードを書きましたが、普通のエンジニアリングとはまた違った試行錯誤が求められて、やはり楽しいですね。

調べてみると、最近はまたいろいろなクリエイティブコーディングツールがでてきているようです。 Rustで書けるnannouなんかも面白そう。

Generative Art人工生命は以前から興味の対象でしたので、また遊びがてら書いていきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Flippy (NFT Barter) | Devpost

GitHub - Japan-DfinityInfoHub/generative-art-nft

GitHub - Japan-DfinityInfoHub/nft-barter

NFTの絵柄を生成するコード · Issue #2 · Japan-DfinityInfoHub/generative-art-nft · GitHub

GitHub - bioerrorlog/p5-in-react: PoC for demo NFT implementation

home | p5.js

Perlin noise - Wikipedia

人工生命 "Lenia" を動かす | ALife

人工生命 "Lenia" を触って遊びます。

はじめに

"Lenia" という美しい人工生命モデルがあります。

このデモ動画を見てみて下さい。

顕微鏡を覗いているような、"何か"が生きているかのような動きが美しいですね。

今回は、この人工生命"Lenia"を手元で動かして遊んでみます。

作業環境

Windows 10
Python 3.7.6
Git bash

で作業しました。

人工生命 "Lenia" を動かす

Leniaとは

Lenia is a system of continuous cellular automata, a form of artificial life. It was derived from Conway's Game of Life by making everything smooth, continuous and generalized.

Lenia Portalより

(意訳) Leniaは、連続値セルオートマトンの人工生命です。 コンウェイのライフゲームから、あらゆる要素をスムーズで連続的に汎化して作られています。

とのことで、ライフゲームベースの時空間的に連続な人工生命モデルとして、Bert Chan氏が作成したものです。

論文もあるので、詳細はこちらを参照ください。

Lenia - Biology of Artificial Life

https://direct.mit.edu/isal/proceedings/isal2020/32/221/98400

Leniaを動かす

それではLeniaを動かしていきます。

github.com

Python, Matlab, Javascriptなど複数の言語がサポートされていますが、今回はPythonバージョンを動かしてみます。

# ソースコードをclone
$ git clone https://github.com/Chakazul/Lenia.git

# Pythonバージョンのディレクトリに移動
$ cd Lenia/Python

# virtualenvがインストールされていない場合はインストールする
$ pip install virtualenv

# 仮想環境作成
$ virtualenv .venv

# 仮想環境有効化
$ source .venv/Scripts/activate

# パッケージインストール
$ pip install -r requirements.txt

これで準備は完了です。

いよいよLeniaを実行してみます。

$ python LeniaND.py 

このようにLeniaのウィンドウが表示されれば成功です。

いろんなLenia

ウィンドウ上部のタブからパラメータを操作すると、いろいろな形態を表現することが出来ます。

いくつか例を紹介します。

非常に多様な形態が創発されて面白いですね。

ぜひいろいろ遊んでみてください。

おわりに

今回は人工生命 "Lenia" を動かして遊びました。

セルオートマトンは古くより研究されてきた分野ですが、このLeniaのようにいかにも"生き物っぽい"動きを目の当たりにすると、この分野の奥深さを実感します。

人工生命のモデルは面白そうなものがたくさんあるので、いろいろと触ってみたいものです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - Chakazul/Lenia: Lenia - Mathematical Life Forms

Lenia Portal

Lenia - Biology of Artificial Life

https://direct.mit.edu/isal/proceedings/isal2020/32/221/98400

Stanford Seminar - Lenia: Biology of Artificial Life | YouTube

Bert Chan

Godot EngineでBoids Flockingを実装する | 人工生命

Boids FlockingシミュレーションをGodot Engineで実装します。

はじめに

Boids Flocking (ボイドモデル / ボイド群衆アルゴリズム) をGodot Engineで実装してみました。

エサを追いかけるような、可愛げのあるboidsたちの動きがなかなかのお気に入りです。


今回は、これらの実装方法のメモを残します。

※追記

今回実装したBoids Flockingをもとに、Boids達から逃げるちょっとしたゲームを作って公開しました。

ブラウザですぐ触れるので、良ければぜひ遊んでみてください。

Boids Flockingとは

まずは、Boids Flockingアルゴリズムについておさらいします。

Boidsとは、鳥の群れの動きをシミュレートする人工生命プログラムです。
"Bird-oid"、「鳥っぽいもの」という意味で、Boidsと名づけられました。 クレイグ・レイノルズが開発し、論文は1987年に発表されています。

画像はクレイグ・レイノルズの論文より引用: Flocks, Herds, and Schools: A Distributed Behavioral Model, Craig W. Reynolds, 1987, Computer Graphics

このBoids Flockingアルゴリズムでは、以下の3つのシンプルな法則によって群れを形成します:

  • Separation: 分離
  • Alignment: 整列
  • Cohesion: 結合


Separation: 分離は、近隣のオブジェクトと近づきすぎないよう距離をとる力です。

Separation: 分離 | 画像はWikipediaより


Alignment: 整列は、近隣のオブジェクトと同じ方向に進もうとする力です。

Alignment: 整列 | 画像はWikipediaより


Cohesion: 結合は、近隣のオブジェクト集合の中心地に向かおうとする力です。

Cohesion: 結合 | 画像はWikipediaより


この3つの力の組み合わせで、オブジェクトの集合が鳥の群れのような挙動をとるようになります。 単純な法則から複雑な現象が沸き起こるところは、生き物の醍醐味ですね。

それでは、以下Godot EngineによるBoids Flockingシミュレーションの実装を見ていきます。

実装

最終的なソースコードは以下のGitHubに置いていますので、良ければまずはそちらをご覧ください。
以下の章では、各要素ごとの実装を取り上げて見ていきます。 github.com

Separation

まず、Separationの実装を見ていきます。 言語はGodot Engineの標準スクリプト言語のGDScriptです。

func process_seperation(neighbors):
    var vector = Vector2()
    var close_neighbors = []
    for boid in neighbors:
        if position.distance_to(boid.position) < perception_radius / 2:
            close_neighbors.push_back(boid)
    if close_neighbors.empty():
        return vector
    
    for boid in close_neighbors:
        var difference = position - boid.position
        vector += difference.normalized() / difference.length()
    
    vector /= close_neighbors.size()
    
    return steer(vector.normalized() * move_speed)

大きく二つの処理からなっています。
一つ目は近隣boidsの取得、
二つ目は近隣boidsからSeparateする力の算出です。

近隣boidsの取得は、以下のコードで行っています:

   var close_neighbors = []
    for boid in neighbors:
        if position.distance_to(boid.position) < perception_radius / 2:
            close_neighbors.push_back(boid)

もともとこの関数に渡されるneighbors自体、一定の範囲perception_radiusを基準に算出された近隣boidsを指しています。 しかし、Separation力の算出源をさらに近い位置にいるboidたちに絞るため、perception_radius / 2以内にいるboidsをclose_neighborsとして登録しています。

そしてこれらclose_neighborsから、以下のようにしてSeparationの力を算出しています:

   for boid in close_neighbors:
        var difference = position - boid.position
        vector += difference.normalized() / difference.length()
    
    vector /= close_neighbors.size()

こうして算出したSeparation力を、本体boidの移動ベクトルに加算するという流れです。

Alignment

続いて、Alignmentを見ます。

func process_alignments(neighbors):
    var vector = Vector2()
    if neighbors.empty():
        return vector
        
    for boid in neighbors:
        vector += boid.velocity
    vector /= neighbors.size()
    
    return steer(vector.normalized() * move_speed)

Alignment力の算出はシンプルです。 neighborsの現在の進行ベクトルの平均をもとにAlignment力を算出し、本体boidの移動ベクトルに返却します。


Cohesion

続いてCohesionです。

func process_cohesion(neighbors):
    var vector = Vector2()
    if neighbors.empty():
        return vector
    for boid in neighbors:
        vector += boid.position
    vector /= neighbors.size()
    
    return steer((vector - position).normalized() * move_speed)

こちらもシンプルで、neighborsの平均座標と本体boidの差分をもとにCohesion力を返却しています。

ここまでが、Boids FlockingモデルにおけるSeparation / Alignment / Cohesionの実装です。 次には補足として、エサを追いかける動作の実装を見ます。

エサを追いかける動作

エサを追いかける動作として、単純にあるポイントへの求心力を算出しています。

func process_centralization(centor: Vector2):
    if position.distance_to(centor) < centralization_force_radius:
        return Vector2()
        
    return steer((centor - position).normalized() * move_speed)   

この引数centorとしてエサの座標を渡すことで、エサへ向かう力を算出できます。 ただし、エサに近過ぎる場合は空Vectorを返却することで、エサにboidが収束してしまうのを防いでいます。


以上、ここまで見てきたそれぞれの力をフレームごとに計算し、以下のように合算して移動ベクトルをとることで、boidsの振る舞いが定義できます。

func _process(delta):
    var neighbors = get_neighbors(perception_radius)
    
    acceleration += process_alignments(neighbors) * alignment_force
    acceleration += process_cohesion(neighbors) * cohesion_force
    acceleration += process_seperation(neighbors) * seperation_force
    acceleration += process_centralization(prey_position) * centralization_force
        
    velocity += acceleration * delta
    velocity = velocity.clamped(move_speed)
    rotation = velocity.angle()
    
    translate(velocity * delta)

おわりに

以上、Boids FlockingシミュレーションをGodot Engineで実装する際のメモを書きました。

処理効率等を考慮したプログラムではありませんので高度な利用はできませんが、彼らが動き出した時の感動はなかなかのものでした。

Boids Flockingは人工生命モデルの中でもシンプルかつ有名なものですが、今後はもっと複雑なモデルを動かせると楽しいだろうなとワクワクしています。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Flocks, Herds, and Schools: A Distributed Behavioral Model, Craig W. Reynolds, 1987, Computer Graphics

GitHub - codatproduction/Boids-simulation: A flocking simulation with obstacle avoidance made in Godot!

Code That: Boids - YouTube

Boids - Wikipedia

GitHub - alifelab/alife_book_src: 「作って動かすALife - 実装を通した人工生命モデル理論入門」サンプルコード

Generative Art #14 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
14作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20200419210659p:plain f:id:BioErrorLog:20200419210715p:plain f:id:BioErrorLog:20200419210741p:plain f:id:BioErrorLog:20200419210804p:plain

フル解像度/その他出力パターンはこちら:
Generative_14 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

Cell[][] _cellArray;
int _cellSize = 1; // Cellの大きさ(pixel)
int _numX, _numY; // ディスプレイのCell格子


void setup() { 
  size(1920, 1080); // ディスプレイサイズ
  _numX = floor(width/_cellSize);
  _numY = floor(height/_cellSize);
  restart();
} 


void restart() {
  // _cellArrayに画面分のCellを入れる
  _cellArray = new Cell[_numX][_numY];    
  for (int x = 0; x<_numX; x++) {
    for (int y = 0; y<_numY; y++) {    
      Cell newCell = new Cell(x, y);  
      _cellArray[x][y] = newCell;      
    }               
  }                 
  
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {  
      
      int above = y-1;        
      int below = y+1;        
      int left = x-1;         
      int right = x+1;            
      
      // 画面端の処理
      if (above < 0) { above = _numY-1; }    
      if (below == _numY) { below = 0; } 
      if (left < 0) { left = _numX-1; }  
      if (right == _numX) { right = 0; } 

     _cellArray[x][y].addNeighbour(_cellArray[left][above]);    
     _cellArray[x][y].addNeighbour(_cellArray[left][y]);        
     _cellArray[x][y].addNeighbour(_cellArray[left][below]);    
     _cellArray[x][y].addNeighbour(_cellArray[x][below]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][below]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][y]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][above]);   
     _cellArray[x][y].addNeighbour(_cellArray[x][above]);       
    }
  }
}


void draw() {
  background(200);
                    
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {
     _cellArray[x][y].calcNextState();
    }
  }
                        
  translate(_cellSize/2, _cellSize/2);     
                        
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {
     _cellArray[x][y].drawMe();
    }
  }

  saveFrame("frames/generative_14_#####.png"); // 各フレームで画像を保存
}


void keyPressed(){
    /*
    BACKSPACEキー押下: setup()を呼ぶ
    */

    if (keyCode == BACKSPACE){
        setup();
    }
}


class Cell {
  float x, y;
  float state;         
  float nextState;  
  float lastState = 0; 
  Cell[] neighbours;
  
  Cell(float ex, float why) {
    x = ex * _cellSize;
    y = why * _cellSize;
    
    // Cellの初期状態を定義
    // 画面の真ん中のCellは確率で255, それ以外は0
    // 255になるのは4つまで
    int initCount = 0;
    if(width/4 < ex && ex < width*3/4 && height/4 < why && why < height*3/4 && initCount <= 4){
      if(random(100000) < 1){
        nextState = 255; 
        initCount += 1;
      }else{
        nextState = 0;
      }
    }else{
      nextState = 0;
    }

    state = nextState;
    neighbours = new Cell[0];
  }
  
  void addNeighbour(Cell cell) {
    neighbours = (Cell[])append(neighbours, cell); 
  }
  
  void calcNextState() {
    /*
    1. そうでなければ、新しい状態=現在の状態+隣接セルの状態の平均ー前の状態の値
    2. もし新しい状態が255を超えたら255にし、
    3. もし新しい状態が0以下ならそれを0にする
    */
                
    float total = 0;              
    for (int i=0; i < neighbours.length; i++) { 
       total += neighbours[i].state;        
    }                   
    float average = int(total/8);
                
    if (1 < average && average < 10) { // ※
      nextState = 255;
    }else{
    nextState = state + average;
      if (lastState > 0) { nextState -= lastState; } // 1.
      if (nextState > 255) { nextState = 255; } // 2.
      else if (nextState < 0) { nextState = 0; } // 3.
    }
    lastState = state;  
  }
  
  void drawMe() {
    state = nextState;
    noStroke();
    fill(state);    
    rect(x, y, _cellSize, _cellSize);
  }
}

Discussion

6作目7作目と同様、セルオートマトンの法則を利用して作成しました。

序盤の規則的な四角い造形から、斜め方向に走るパターンが現れ、徐々に雨降る道路の水たまりのような、複雑な状態になっていく過程が面白いところです。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work


Generative Art #13 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
13作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20200217220148p:plain f:id:BioErrorLog:20200217220210p:plain f:id:BioErrorLog:20200217220226p:plain

フル解像度/その他出力パターンはこちら:
Generative_13 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

/*
Reference:
ジェネラティブ・アート―Processingによる実践ガイド - マット・ピアソン
*/


int _numChildren = 6; 
int _maxLevels = 6;  

Branch _trunk;

void setup() { 
    size(1920,1080); 
    background(255); 
    noFill(); 
    smooth(); 
    newTree();
}

void newTree() { 
    _trunk = new Branch(1, 0, width/2, height/2); 
    _trunk.drawMe();
}

void draw() { 
    background(255); 
    _trunk.updateMe(width/2, height/2); 
    _trunk.drawMe();

    saveFrame("frames/generative_13_#####.png"); // 各フレームで画像を保存
}

void keyPressed(){
    /*
    BACKSPACEキー押下: リセット
    */
    
    if (keyCode == BACKSPACE){
        setup();
    }
}

class Branch { 
    float level, index; 
    float x, y; 
    float endx, endy;
    float strokeW, alph; 
    float len, lenChange; 
    float rot, rotChange;

    Branch[] children = new Branch[0];

    Branch(float lev, float ind, float ex, float why) {

        level = lev; 
        index = ind;
        strokeW = (1/level) * 3; 
        alph = 5; 
        len = (1/level) * random(1000); 
        rot = random(360); 
        lenChange = random(1) - 1; 
        rotChange = random(0.5) - 0.5;
        
        updateMe(ex, why);

        if (level < _maxLevels) { 
            children = new Branch[_numChildren]; 
            for (int x=0; x<_numChildren; x++) { 
                children[x] = new Branch(level+1, x, endx, endy);
            } 
        } 
    }


    void updateMe(float ex, float why) { 
        x = ex; 
        y = why;

        rot += rotChange; 
        if (rot > 360) { 
            rot = 0; 
        } else if (rot < 0) { 
            rot = 360; 
        }

        len -= lenChange; 
        if (len < 0) { 
            lenChange *= -1; 
        } else if (len > 100) { 
            lenChange *= -1; 
        }

        float radian = radians(rot); 
        endx = x + (len * cos(radian)); 
        endy = y + (len * sin(radian));
        for (int i=0; i<children.length; i++) { 
            children[i].updateMe(endx, endy);
        }
    }

    void drawMe() {
        if (level > 1) { 
            strokeWeight(strokeW); 
            stroke(0, alph); 
            fill(50, alph); 
            ellipse(endx, endy, noise(len)*100, noise(len)*100);
        } 
        
        for (int i=0; i<children.length; i++) { 
            children[i].drawMe();
        } 
    }
}

Discussion

前回同様、マット・ピアソン「ジェネラティブ・アート―Processingによる実践ガイド」7章で紹介されるフラクタル構造のケーススタディを改変して作成。

フラクタル状の「宇宙のゼンマイ時計」の節々に薄く円を描画し、それぞれの円をnoiseで振動させています。

せわしなく動くクラゲの大群のような、あるいは解像度の低い煙のような雰囲気が面白いです。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference


Generative Art #12 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
12作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20200211172921p:plain f:id:BioErrorLog:20200211172940p:plain f:id:BioErrorLog:20200211172954p:plain f:id:BioErrorLog:20200211173011p:plain

フル解像度/その他出力パターンはこちら:
Generative_12 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

/*
Reference:
ジェネラティブ・アート―Processingによる実践ガイド - マット・ピアソン
*/


int _numChildren = 6; 
int _maxLevels = 6;  

Branch _trunk;

void setup() { 
    size(1920,1080); 
    background(0); 
    noFill(); 
    smooth(); 
    newTree();
}

void newTree() { 
    _trunk = new Branch(1, 0, width/2, height/2); 
    _trunk.drawMe();
}

void draw() { 
    background(0); 
    _trunk.updateMe(width/2, height/2); 
    _trunk.drawMe();

    saveFrame("frames/generative_12_#####.png"); // 各フレームで画像を保存
}

void keyPressed(){
    /*
    BACKSPACEキー押下: リセット
    */
    
    if (keyCode == BACKSPACE){
        setup();
    }
}

class Branch { 
    float level, index; 
    float x, y; 
    float endx, endy;
    float strokeW, alph; 
    float len, lenChange; 
    float rot, rotChange;

    Branch[] children = new Branch[0];

    Branch(float lev, float ind, float ex, float why) {

        level = lev; 
        index = ind;
        strokeW = (1/level) * 3; 
        alph = 255; 
        len = (1/level) * random(10); 
        rot = random(360); 
        lenChange = random(1) - 1; 
        rotChange = random(0.5) - 0.5;
        
        updateMe(ex, why);

        if (level < _maxLevels) { 
            children = new Branch[_numChildren]; 
            for (int x=0; x<_numChildren; x++) { 
                children[x] = new Branch(level+1, x, endx, endy);
            } 
        } 
    }


    void updateMe(float ex, float why) { 
        x = ex; 
        y = why;

        rot += rotChange; 
        if (rot > 360) { 
            rot = 0; 
        } else if (rot < 0) { 
            rot = 360; 
        }

        len -= lenChange; 
        if (len < 0) { 
            lenChange *= -1; 
        } else if (len > 100) { 
            lenChange *= -1; 
        }

        float radian = radians(rot); 
        endx = x + (len * cos(radian)); 
        endy = y + (len * sin(radian));
        for (int i=0; i<children.length; i++) { 
            children[i].updateMe(endx, endy);
        }
    }

    void drawMe() {
        if (level > 1) { 
            strokeWeight(strokeW); 
            stroke(255, alph); 
            fill(100, alph); 
            ellipse(endx, endy, noise(len)*100, noise(len)*100);
        } 
        
        for (int i=0; i<children.length; i++) { 
            children[i].drawMe();
        } 
    }
}

Discussion

マット・ピアソン「ジェネラティブ・アート―Processingによる実践ガイド」7章で紹介されるフラクタル構造のケーススタディを改変して作成。

フラクタル状の「宇宙のゼンマイ時計」の節々に円を描画し、それぞれの円をnoiseで振動させています。

生き物の初期胚が必死さが見えるような、少し神秘的でどこか微笑ましい雰囲気が気に入っています。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference


人工生命をつくりたい - 思うところとアプローチのメモ | ALife

生き物をつくりたい、そのモチベーションと必要そうな知識 / アプローチのメモを書き残します。


こんにちは、@bioerrorlogです。

まず冒頭に、今回の記事を書くに至った個人的所感、背景を書き出します。 必要に応じて本編まで飛ばしてください。

思うところ/個人的背景

個人的な話ですが、幼い頃から虫が好きでした。 道端や空き地に住んでいる彼らは小さく愛らしい一方、不気味で予測不能なところが時間を忘れるほど面白いものでした。

この生き物への興味を持ったまま、大学ではその一分野を研究しました。 面白いデータに出会ったときや膨大な数字の中からパターンを解析したときなど、大いに感動・勉強になった経験がたくさんあります。

しかし、小さい頃に感じたような腹が浮き上がるような好奇心までは感じられなかった、というのが正直なところでした。 その理由のひとつは恐らく、還元主義的な考え方に疑問があったからだと今振り返って思います。

「生物を形作るひとつひとつの遺伝子機能を解明しても、その集合結果たる生物そのものを理解することはできないのでは」ということです。

ある遺伝子の機能をひとつ明らかにすることは、もちろん人類の叡知を一歩拡大する素晴らしいことです。 自分の研究が形になったときは、大きな喜びと充実感が溢れました。

しかし、何万と解明されている遺伝子の機能に何万一個目を追加しても、生命がこの手に感じられるとは思えなかったのもまた事実です。


さて、のちに私はITの世界に飛び込みました。 何でも自分のロジックで作ることができるというのは、この世界の強みです。

材料費も、(生物のような)交配時間も必要なく、知識や情報さえあれば何でも作ることが出来ます。 コンピュータのパワーは、年々掛け算の速度で高まっています。 新しい技術も、日進月歩で進化しています。


ここでひとつ思ったことがあります。

つまり、「生き物も作れるのでは」ということです。

前置きが長くなりましたが、伝えたかったのはこのことです。

生き物をつくりたいのです。

要素ごとに分割し、その一つ一つを解明しても生命を感じられないのであれば、作ればいいのではないか。 目覚ましい技術・研究の進歩は、それを可能な範囲にまで呼び寄せているのではないか。


とはいえ、現時点での私は何の背景知識も、良い考えもありません。 そこで今回は第一歩として、生き物をつくるために着目すべき分野/キーワード/アプローチを思いつくままに書き並べていき、頭の中を整理していきます。

聖書の世界観では、神が最初の人類を創造する | ミケランジェロ 「アダムの創造」 / Public domain

アプローチ

人工生命

まずは何より、人工生命という研究分野を調べる必要があります。

人工生命 (Artificial Life, A-Life, ALife, AL)とは、その言葉を生み出し、第一回人工生命国際会議を主催したクリストファー・ラングトンによると*1

The term Artificial Life literally means "life made by humans rather than by nature."

文字通り、自然によってではなく人間によって作られた生命のことです。

クリストファー・ラングトン | Wikipediaより

面白いのは、ここでいう「生命」という言葉のとらえ方です。 これは、地球上に「すでに存在する生命(life-as-we-know-it)」だけを指しているのではなく、現実世界では生まれ出ることのなかった「ありえたはずの生命(life-as-it-could-be)」をも含んだ言葉として使われているのです。

つまり、私たちが目にしている「すでに存在する生命」は、無数に存在する生命の可能性のひとつに過ぎない、という解釈です。

漠然と新鮮な切り口を求めていた私としては、これはとても魅惑的な考え方でした。


この人工生命という分野にどのような研究があるか、その方位を描くとすれば、次のような二つの軸がわかりやすいと個人的に思っています。

一つ目は、「弱い」人工生命 /「強い」人工生命という軸です*2

「弱い」人工生命は、生命の一側面をシミュレートしようとする考え方です。 「すでに存在する生命」あるいは「ありえたはずの生命」をシミュレートし、それらをより深く理解しようとすることを主眼に置きます。

一方、「強い」人工生命は、文字通り人間の手によって「生きている」生物を作ろうとするものです。

私としてはどちらも大いに興味があるので、隔てなく調べる価値があると思っています(特に後者の研究がどれほど進んでいるのかには興味があります)。


二つ目の軸は、人工生命を実現する手段です。 これによって、ハードウェア / ソフトウェア / ウェットウェアの3つの区分に分けることが出来ます*3

ハードウェアを用いた人工生命は、ロボットやコンピュータアーキテクチャを対象としています。

ソフトウェアを用いた人工生命は、コンピュータ上でのモデルを対象としています。

ウェットウェアを用いた人工生命は、生化学的手法を対象としています。

私としては、ソフトウェアを対象とした人工生命にスポットを当てていきたいと思っています。 ハードウェア/ウェットウェアももちろん興味ありますが、自分一人でやるのには時間・費用が間に合いません。


今はこの分野に対してこのようなぼんやりしたイメージですが、キーとなる研究や書籍にあたっていくことで、少しずつ何かが得られればという思いです。


有名な人工生命モデル、"Tierra"を実行してみた動画:

複雑系

次は複雑系という分野についてです。

この分野については正直何も知らないのですが、生命の本質を語るには避けて通れない考え方のようです。


生命はどのような状態に生じうるのか。
クリストファー・ラングトンは情報の複雑性に着目し、「カオス」と「秩序」の中間点「カオスの淵」に生命はあると考えたそうです。

クリストファー・ラングトンによる複雑性の分類。図はnode99.orgより引用

この図について、スティーブン・レビーの書籍「人工生命-デジタル生物の創造者たち」の解説を引用します。

左側は情報が凍り、何もが生きられない場合。 右に進むと結晶が生じるようなより柔軟性のある領域があるが、情報の移動は限られ、ここでも生命は維持できない。 右端まで行ってしまうと、情報があまりに自由に動きすぎてその構造が維持不能になり、あまりに無秩序なので生命は維持できない。 中央部の「スイート・スポット」といえる部分だけで、情報はその発信構造を維持できるほど安定し、発信できる程度にゆるやかだ。 生命はそこにいる。

完全なカオスでもなく、結晶のように規則的でもない、その中間点に生命は存在する、という話は、直感的にうなずけます。

しかし、私には直感的にしかわかりません。

複雑系という考え方、その原理や既知の事項を知れば、生命とは何ぞやという疑問に対して、情報という側面からの見方ができるようになると感じています。


人工知能

いま流行りの人工知能ですが、人工生命とも密接に関わっていくのではと感じています。

実際、ディープラーニングのパイオニアとして有名なジェフリー・ヒントンや、UberのAI研究所トップであるケネス・スタンリーなど、いま大活躍している人工知能研究者の中にも人工生命から影響を受けた方が多くいます*4

また、2020年のALife conferenceのテーマが “New frontiers in AI: What can ALife offer AI?” であるところを見るに、人工生命側からの人工知能への関心も高まってきているようです。

2020年 ALife conferenceの表紙。画像はカンファレンスホームページより引用


また個人的には、OpenAIのIgor Mordatchの研究が強く印象に残っています。

複数の人工知能エージェントの相互作用を用いた数々の研究を行っており、最近ではエージェントにかくれんぼをやらせる研究が面白かったです。 鬼と子に分かれたエージェントが対戦を繰り返して上達していくうちに、想定されていなかった方法でエージェントがふるまい始めるようになります。

鬼(赤)のエージェントが自ら箱に乗ったまま箱を移動させるという予想外の方法を見出した。Emergent Tool Use From Multi-Agent Autocurricula, Bowen Baker et al., 2019, Fig. 1より引用

設計者の意図しなかったふるまいを始めるというのは、まるで世代を跨ぐ生物の進化のような、単なる最適化で終わらない面白さが伺われます。


人工知能は大きな研究分野ですが、ところどころをピックアップして調べていくと面白そうです。


コンピューターウイルス

コンピューターウイルスは、人工生命をもっともよく体現している具体例のひとつと言えるのではないでしょうか。

これらは自己増殖し、変異し、複雑で、しばしば制御不能です。

実際、人工生命とコンピュータ-ウイルスを結び付けた研究も数多く行われてきたようです。 Google Scholarでちょっと調べただけでも、多くの論文が見当たります。

偶然にも、コンピューターセキュリティについては好きで少し調べていたこともあります

コンピューターウイルスの原理を理解することで、人工生命の具体的なイメージの一例が得られるかも知れません。


近年猛威を奮ったランサムウェア "WannaCry" | Wikipediaより

アート

最後は、アートの世界についてです。

人工生命には、ハードウェア / ソフトウェア / ウェットウェアの3つやり方があると前述しましたが、4つ目のフィールドとしてアートを掲げる向きもあるようです*5

実際、2018年のALife conferenceでは、アートアワードが開催されています。

Lenia – Mathematical Life Forms from Bert Chan on Vimeo.

例えばこの "Lenia – Mathematical Life Forms" はアートアワード受賞作品の一つで、連続的時空間による二次元セルオートマトンです。 論文ソースコードも公開されているので、ぜひ自分でも触ってみたいものです。


個人的には最近、ジェネラティブ・アート(Generative Art)というものにも注目しています。

ジェネラティブ・アートとは、自分の手で絵を描くのではなく、プログラムなどを用いて与えられたアルゴリズムをもとに、生成的に描かれるアート作品です。

簡単なアルゴリズムから生まれる複雑で予想外に美しい絵は、見ていても、作っていても、時間を忘れられるほど面白いものです。

最近描いたジェネラティブアート作品のひとつ。ふわふわな感じが面白い。

私が特に好きなのは、ジェネラティブ・アートの根底にある考え方です。 マット・ピアソンは著書「ジェネラティブ・アート - Processingによる実践ガイド」で、繰り返しジェネラティブ・アートとカオスとの関係性に触れています。

私の考えでは、ジェネラティブ・アートは、光とピクセルのバレエで表されるような、自然な調和をひねりだそうとするカオスと秩序の力の間の、永遠で巨大な戦いの副産物にすぎません。


私たちは、有機性(オーガニック)と機械性(メカニカル)の間、カオスと秩序の間の、ちょうど良くバランスがとれたスイートスポットを探しています。

「カオスと秩序の間のスイートスポット」という考えは、まさしくクリストファー・ラングトンの提唱したカオスの淵、生命のいる場所と一致しています。 一見関係ない二つの分野でこのような同じ考え方が見られるのは、心躍ることです。


このようにアートは、人工生命の有効な出力形式であり、生命を肌に感じる良いアプローチなのでは、と感じています。


人工生命モデルのひとつ、セルオートマトンを利用して描いた絵

おわりに

生き物をつくるために必要そうな知識やアプローチを、思いつくままに書き並べました。

これは、第一歩時点での感情を書き残した、メモのようなものにすぎません。

やりたいことはたくさんありますので、肩の力を抜いて気軽にやっていきます。

関連記事

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

*1:Artificial life: An overview, CG Langton, 1997 を参照

*2:人工生命-デジタル生物の創造者たち, スティーブン・レビー, 1996 を参考にしました

*3:作って動かすALife, 岡瑞樹 et al., 2018 を参考にしています

*4:Introduction to Artificial Life for People who Like AI, Lana Sinapayen, 2019, The Gradientを参照

*5:再び、Introduction to Artificial Life for People who Like AI, Lana Sinapayen, 2019, The Gradientを参照

Processingの始め方 | チュートリアル

はじめてProcessingを触る方に向けて、Processingの始め方をチュートリアル形式で書いていきます。

Processingの環境構築から、出力した作品を画像ファイルとして保存する方法までを辿っていきます。


はじめに

おはよう。@bioerrorlogです。

最近、Processingというプログラミング言語を使って絵を描きはじめました。

自分の手に筆を持って描くのではなく、コードを叩いて絵を描画するというのは、絵心に欠ける私にも簡単で、何より楽しいものです。 www.bioerrorlog.work

同じようにProcessingを使って何かをしてみたいという方にむけて、Processingの始め方を、私の経験をもとに書き残していきます。


Processingの始め方

Processingとは

Processingとは、公式サイトによると、

Processing is a flexible software sketchbook and a language for learning how to code within the context of the visual arts.

Processingはビジュアルアートのためのプログラミング言語」という感じです。

アート向けといっているだけあって描画のための機能があらかじめ多く備わっており、プログラミングの知識がなくてもすぐに絵を描き始めることができます。

そのため、絵を描きたい方にだけでなく、プログラミングを未経験から始めたいけどどこから手を付ければいいかわからない、という方にもProcessingはおすすめです。 Processingコードの文法は直感的にわかりやすく、何よりコードの実行結果が視覚的にすぐ返ってくるからです。

自分の書いたコードの実行結果が視覚的にすぐ得られると、プログラミングを楽しみやすいかと思います。
(ほかの言語でプログラミングを始めた場合、おそらく一番最初にやるのは変数の代入や演算子の使い方などになるでしょう。 正直、それを楽しいと感じられる方は少ないのではないでしょうか。)

では、さっそくProcessingをインストールしていきます。


Processingのインストール

1.公式サイトからProcessingダウンロードページに行きます

f:id:BioErrorLog:20191215115830p:plain


2.該当するProcessingパッケージをダウンロードします

f:id:BioErrorLog:20191215120036p:plain

Windowsの場合、64-bitと32-bitの2つのバージョンがあります。 これは自身のPCに入っているWindowsに合わせて選ぶ必要があります。
Windowsが64-bitなのか32-bitなのかを調べるには、

  • Windows10の場合:
    1.Windowsボタンから「設定(歯車ボタン)」をクリック
    2.「システム」をクリック
    3.左のタブから「バージョン情報」をクリック
    4.「デバイスの仕様」のなかの「システムの種類」から64 bitか32ビットかを確認

  • それ以外(Windows8や7)の場合:
    こちらを参考に: Microsoftサポート


3.Processingパッケージを展開します

Processingパッケージをダウンロードしたら、それをどこかに展開すればProcessingを使う準備は完了です。
ダウンロードしたProcessingパッケージ(zipファイル)を右クリック -> "すべて展開"を選べば、ファイルを展開することが出来ます(展開する場所はどこでも大丈夫です)。

展開されたファイルの中に"processing.exe"があるので、それをダブルクリックするとProcessingが立ち上がります。

f:id:BioErrorLog:20191215125220p:plain

このようなウィンドウが立ち上がれば準備O.K.です。


はじめてのProcessing

さっそく、なんか書いていきましょう。
例えば、次のようなのものはいかがでしょうか。

size(1920, 1080);
ellipse(1000, 500, 1000, 1000);

たった2行のシンプルなコードです。

1行目size(1920, 1080);は、出力するウィンドウのサイズを指定しています。
括弧内の2つの数字 (引数と呼ばれます) は、size(幅, 高さ)を指定しています。

今回は私が好きな1920x1080のサイズにしてみました(私のPCのディスプレイサイズです。 このサイズで出力すると、気に入ったものをPCの壁紙にするのに丁度よいのです)。

2行目ellipse(1000, 500, 1000, 1000);はellipse(楕円)を描画するためのコードです。
引数はそれぞれ、ellipse(x座標, y座標, 幅, 高さ)を指定しています。

ここで、Processingにおけるxy座標には少し注意が必要です。 Processingでは座標の原点は左上となっており、右に行くにつれてxが増加、下に行くにつれてyが増加するような定義になっています。

f:id:BioErrorLog:20191229181351p:plain
Processingにおけるxy座標の例


また、注意が必要な点としてはもうひとつ、それぞれの行末についているゼミコロン;を忘れないでください。
このセミコロンを書き忘れると、コードがエラーになってしまいます。 実際、私はあまりに頻繁にこのセミコロンを書き忘れるので、エラーが起きたときはすぐにセミコロンの書き忘れを確認する癖がついてしまいました。


さて、それではさっそくコードを実行してみます。

Processingエディターの左上にある再生ボタンを押せば、コードが実行されます。 次のような、(上端が少し見切れている)美しい円が出力されたでしょうか。

f:id:BioErrorLog:20191229184524p:plain

無事出力されれば、はじめてのProcessing作品の制作は成功です。

つぎは、この簡単なコードを発展させて、もっといい感じの絵に仕上げていきます。


もっといい感じのProcessing

それでは、コードにもう少し手を加えていきます。

例えば、次のようなのはどうでしょう。

int x;
int y;

void setup(){
    size(1920, 1080);
    background(255);

    fill(0, 100);
    noStroke();

    frameRate(5);
    
    x = 0;
    y = height / 2;
}

void draw(){
    ellipse(x, y, 300, 300);
    x += 150;
}

先ほどの2行のコードに比べれば少し長くて複雑ですが、ひとつひとつ見ていきますので安心してください。


まずは冒頭のこちら。

int x;
int y;

これは、変数の宣言です。
2つの整数(int)、xyをこのプログラムで使うことを宣言しています。
変数とは、数を格納しておく箱のようなもので、以後このプログラムの中ではこれらに値を代入したり、足し算引き算などの演算を行うことが出来ます。

今回はこの変数xyを、円を描写するxy座標として使います。


次は、setupdrawという2つの関数を見ていきます。

void setup(){
    // 関数内の処理
}

void draw(){
    // 関数内の処理
}

関数とは、ざっくり言えば一連の処理をまとめたものです。 setupdrawそれぞれの中に書かれた処理が、それぞれの関数のくくりで実行されます。

そしてこのsetupdrawという関数は、Processingによってあらかじめ定義されたタイミングで実行される関数です。

setup関数は、プログラムが起動されたときに一度呼び出される関数です。 主に、初期状態やモデルの設定を定義する処理をここに書いておきます。

draw関数は、プログラムの実行中に繰り返し呼び出される関数です。 このそれぞれの繰り返しはフレームと呼ばれ、描画処理をここに書いておけばフレームごとに変化するアニメーションが出力されます。

つまり、プログラムの流れとしては、

実行ボタン -> setup -> draw -> draw ->・・・-> 停止ボタン

となります。


さて、大まかな流れが分かったところで、一つ一つの処理内容を見ていきます。

まずはsetup関数に書かれている処理からいきます。


size(1920, 1080);

これは前章でも使いましたが、出力するウィンドウサイズを指定しています。
size(横幅, 高さ);


background(255);

背景の色を指定しています。
0 - 255 (黒 - 白) の間で値を指定しています。
(もちろん、もう少し複雑な定義をすれば様々な色や透明度を指定できますが、今回は単純に一つの引数でグレースケールを定義しています)
background(グレースケール);


fill(0, 100);

図形の色を指定しています。
この後、ellipse関数で円を描画していきますが、その円の色を定義しています。 2つ目の引数100は透明度を指定しており、図形を透明にすることによって重ね合わせを綺麗に出力できるようにしました。
(透明度は、0 - 255 ( 薄い - 濃い )で指定できます)
fill(グレースケール, 透明度);


noStroke();

図形の外周線を描画しないことを指定しています。
noStroke関数を呼んでおくことで、ellipse関数で円を描画したときにその円の淵に線を引かないようにします。


frameRate(5);

フレームレートを指定しています。
先ほど、draw関数はsetup関数を呼び出した後に繰り返し実行されると書きましたが、どのくらいの頻度でdraw関数を呼び出すか(これをフレームレートといいます)を指定できます。
frameRate関数に渡す引数によって、1秒間に何回draw関数を呼び出すかが指定されます。
frameRate(フレームレート);


x = 0;
y = height / 2;

変数に値を代入しています。
前に書いた通り、今回は変数xyを円を描画するxy座標として使っていきます。
x = 0;は、初期状態としてxに0を代入し(x座標の0は左端を意味していることを思い出してください)、
y = height / 2;は、yを出力ウィンドウの高さの半分に指定しています。

heightという変数はProcessingによってあらかじめ決められた変数で、出力ウィンドウの高さが自動で格納されています。
似たような変数にはwidthというものもあり、これは出力ウィンドウの横幅が自動で格納されています。
絵を描画するのに便利なこのような機能が多く提供されているのは、Processingのうれしいところです。


[関連記事] Generative Art 自作品まとめ - Processing


さて、次はdraw関数の中身を見ていきます。

ellipse(x, y, 300, 300);

前章で紹介したように、楕円を描画するためのコードです。
これをdraw関数に記述すると、フレームごとに楕円が描画されます。
xy座標を指定する引数には、あらかじめ定義していた変数xyを渡しています。
ellipse(x座標, y座標, 幅, 高さ)


x += 150;

肝心なのがこの処理です。
変数x150加算しています。
これをdraw関数に記述することで、、フレームごとにxが加算され、楕円のx座標が増加、つまり描画する円が右に移動していきます。


さて、コードの解説は以上です。
さっそく実行してみます。

f:id:BioErrorLog:20191230142227p:plain

灰色の円が左から右へと描画されていき、なんだか可愛気のある絵が出来上がりました。

さらにコードを工夫していけば、もっと美しい絵を描くことが出来るでしょう。 しかし今回は一旦これで満足して、次はこの絵を保存するやり方を見ていきます。


出力画像を保存する

Processingの出力は、当然ながらプログラムを終了させれば消えてしまいます。
せっかくの作品を大事にとっておくため、出力を保存してみましょう。

ありがたいことに、Processingは保存のための関数を用意してくれています。

主要な関数は、savesaveFrameです。


save関数は非常にシンプルです。
例えば次のように引数に保存するファイル名を指定すれば、画像ファイルを保存することが出来ます。

save("sample_name.png");

この例ではPNG(.png)画像で保存されていますが、ほかにも保存できるファイル形式としては、TIFF (.tif)、TARGA (.tga)、JPEG (.jpg)があります。

保存場所は、スケッチフォルダーの中になります。
スケッチフォルダーは、Processingエディタのツールバーの「スケッチ」から、「スケッチフォルダーを開く」で開くことが出来ます。

このスケッチフォルダーは後から見つけ出すのが面倒くさいので、気に入った出力画像ファイルはどこか自分のわかりやすい場所に移しておくことをお勧めします。

このsave関数は、自分の好きなタイミングで呼び出せるようにしておけばより使い勝手が上がります。
例えば次のようにしておけば、好きなタイミングでキーボードのキーを押し、そのタイミングで画像を保存することが出来ます。

void keyPressed(){
    if (keyCode == ENTER){
        save("sample_name.png");
    }
}

keyPressed関数は、プログラム実行中にキーが押下されたときに呼び出される関数です。
上の例では、ENTERキーが押されたときにsave関数が呼び出されるようになります。
これで、プログラムの経過を観察しながら、お好みのタイミングで保存ができます。


saveFrame関数は、フレームごとに画像を保存することが出来る関数で、出力結果を動画にしたい時などに便利です。
使い方はsave関数とほとんど一緒ですが、一点だけ、保存ファイル名に####のようにシャープを含める必要があります。

saveFrame("sample_name_####.png");

この####にはフレームカウント(今何番目のフレームか)が代入されます。

saveFrame関数をdraw関数に書いておけば、プログラムの開始から終了まですべてのフレームを保存することが出来ます。
keyPressed関数に入れておけば、自分の好きなタイミングでフレームを保存することが出来ます。

自分の用途に合わせて、好きなやり方で自由に保存してみてください。


以上で、今回のチュートリアルを終わります。
最後に少しだけ、この後どんなことをするといいのか、について私の感想を書き残したいと思います。


次は何をすればよいか?

まずは、自分で思うままにコードを書いてみることをお勧めします。

Processingでどのような関数が用意され、引数にはどのような意味があるのかは、ProcessingのAPIリファレンスにすべて書いてあります。
英語で書かれていますが、シンプルな文章なので是非目を通してみてください。
気ままにコードを書きながら、あれこれと遊んでるうちに少しずつProcessingにも慣れてくると思います。


ある程度慣れてきたら、ほかの人の作品を眺めてみるのもいいと思います。

OpenProcessingでは、大量の作品とそのProcessingコードを見ることが出来ます。

redditのProcessing板generative板でも、美しい作品が毎日投稿されています。

またよろしければ、私の作った作品もぜひ参考にしてみてください。 すべてソースコードも公開しています。


まとまった解説が見たければ、書籍を読むのもいいと思います。

私は、マット・ピアソンの「ジェネラティブ・アート - Processingによる実践ガイド」を読みました。
コードの解説だけでなく、背景にある哲学が気さくに語られていて、とても読みやすく楽しい本でした。


おわりに

それでは、以上で記事を終えます。

実のところ、私もProcessingを始めたのはつい数週間前のことでして、まだまだ歩き始めたばかりの初心者です。

初心者としての経験をもとに、同じ立場の方に向けてこのチュートリアル記事を書きました。

誰かの役に立てば幸いです。

Generative Art #11 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
11作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20191223174107p:plain f:id:BioErrorLog:20191223174025p:plain f:id:BioErrorLog:20191223174000p:plain f:id:BioErrorLog:20191223174052p:plain

フル解像度/その他出力パターンはこちら:
Generative_11 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

/*
Modified from:

Daniel Shiffman
Coding Challenge #24: Perlin Noise Flow  Field
https://youtu.be/BjoM9oKOAKY
https://github.com/CodingTrain/website/tree/master/CodingChallenges/CC_024_PerlinNoiseFlowField
*/


FlowField flowfield;
ArrayList<Particle> particles;


void setup() {
  size(1920, 1080);
  
  flowfield = new FlowField(2); // 引数からflowfieldを構成するベクターの数が算出・定義される
  flowfield.update();

  particles = new ArrayList<Particle>();
  for (int i = 0; i < 1000000; i++) {// 生成するParticlesの数を指定
    PVector start = new PVector(random(width), random(height));
    particles.add(new Particle(start, random(2, 8)));
  }
  background(255);
}


void draw() {
  println(frameCount);
  background(255);
  flowfield.update();
  
  for (Particle p : particles) {
    p.follow(flowfield);
    p.run();
  }  

  saveFrame("frames/generative_10_#####.png"); // 各フレームで画像を保存
}


void keyPressed(){
    /*
    BACKSPACEキー押下: リセット
    */
    
    if (keyCode == BACKSPACE){
        setup();
    }
}


public class FlowField {
  /*
  画面に配置されるベクターの集合体
  画面中央に向かう(ただしnoiseの影響を受ける)
  Particlesの動きに力を加える
  */
  
  PVector[] vectors;
  int cols, rows;
  float inc = 0.1;
  float zoff = 0;
  int scl;
  
  FlowField(int res) {
    scl = res;
    cols = floor(width / res) + 1;
    rows = floor(height / res) + 1;
    vectors = new PVector[cols * rows];
  }

  void update() {
    // flowfield状態を定義
    // フレームごとに呼び出される

    float xoff = 0;
    for (int y = 0; y < rows; y++) { 
      float yoff = 0;
      for (int x = 0; x < cols; x++) {

        // add: 各ベクターにノイズを加えるためのベクター
        float angle = noise(xoff, yoff, zoff) * radians(360*2); // ここでベクターの角度を定義
        PVector add = PVector.fromAngle(angle); // ベクター生成
        add.setMag(50); // ノイズ用ベクターの大きさを定義

        // v: 本命のベクター
        // 画面中央に向かう
        PVector v = new PVector(cols/2-x, rows/2-y, 0);
        v.add(add);
        v.setMag(random(0.01)); // ベクターの大きさは小さくする

        int index = x + y * cols;
        vectors[index] = v;
       
        xoff += inc;
      }
      yoff += inc;
    }
    zoff += 0.004;
  }
}


public class Particle {
  /*
  ひとつの粒子
  動きはflowfieldのベクターから影響を受ける
  */

  PVector pos;
  PVector vel;
  PVector acc;
  PVector previousPos;
  float maxSpeed;
   
  Particle(PVector start, float maxspeed) {
    maxSpeed = maxspeed;
    pos = start;
    vel = new PVector(0, 0);
    acc = new PVector(0, 0);
    previousPos = pos.copy();
  }
  void run() {
    update();
    edges();
    show();
  }
  void update() {
    pos.add(vel);
    vel.limit(maxSpeed);
    vel.add(acc);
    acc.mult(0);
  }
  void applyForce(PVector force) {
    acc.add(force); 
  }
  void show() {
    // 描画処理

    noStroke();
    fill(0, 50); // ここで粒子の色を調整
    ellipse(pos.x, pos.y, 1, 1); // ここで粒子の大きさを調整
    updatePreviousPos();
  }

  void edges() {
    // 画面端の処理

    if (pos.x > width) {
      pos.x = 0;
      updatePreviousPos();
    }
    if (pos.x < 0) {
      pos.x = width;    
      updatePreviousPos();
    }
    if (pos.y > height) {
      pos.y = 0;
      updatePreviousPos();
    }
    if (pos.y < 0) {
      pos.y = height;
      updatePreviousPos();
    }
  }

  void updatePreviousPos() {
    this.previousPos.x = pos.x;
    this.previousPos.y = pos.y;
  }

  void follow(FlowField flowfield) {
    // Flowfieldから受ける力を適用
    
    int x = floor(pos.x / flowfield.scl);
    int y = floor(pos.y / flowfield.scl);
    int index = x + y * flowfield.cols;
    
    PVector force = flowfield.vectors[index];
    applyForce(force);
  }
}

Discussion

前回同様、Daniel ShiffmanのPerlin Noise Flow Fieldの実装コードを参考にしました。

画面に格子状に配置されるベクターを定義するFlowfieldクラスを用いて、画面中央に向かう流れをつくりました。 ちょっとした遊び心として、各ベクターにはnoiseによる揺らぎを与えています。

そこにParticleクラスを用いて、1,000,000個の粒子を配置し、動きを動画化しました。

Flowfieldによる流れをごく小さくすることによって、粒子が中央に集まったあともそのまま通り過ぎては拡散し、そしてまた中央へ収縮をはじめる、という繰り返しを見ることが出来ます。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference

Coding Challenge #24: Perlin Noise Flow Field - Daniel Shiffman

Coding Challenge #24: Perlin Noise Flow Field - Daniel Shiffman - GitHub


Generative Art #10 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
10作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20191223162955p:plain f:id:BioErrorLog:20191223163037p:plain f:id:BioErrorLog:20191223163052p:plain

フル解像度/その他出力パターンはこちら:
Generative_10 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

/*
Modified from:

Daniel Shiffman
Coding Challenge #24: Perlin Noise Flow  Field
https://youtu.be/BjoM9oKOAKY
https://github.com/CodingTrain/website/tree/master/CodingChallenges/CC_024_PerlinNoiseFlowField
*/


FlowField flowfield;
ArrayList<Particle> particles;


void setup() {
  size(1920, 1080);
  
  flowfield = new FlowField(10); // 引数からflowfieldを構成するベクターの数が算出・定義される
  flowfield.update();

  particles = new ArrayList<Particle>();
  for (int i = 0; i < 10000; i++) { // 生成するParticlesの数を指定
    PVector start = new PVector(random(width), random(height));
    particles.add(new Particle(start, random(2, 8)));
  }
  background(255);
}


void draw() {
  flowfield.update();
  
  for (Particle p : particles) {
    p.follow(flowfield);
    p.run();
  }
}


void keyPressed(){
    /*
    ENTERキー押下: 画像を保存する
    BACKSPACEキー押下: リセット
    */
    
    if (keyCode == ENTER){
        saveFrame("generative_10_####.png");
    }
    if (keyCode == BACKSPACE){
        setup();
    }
}


public class FlowField {
  /*
  画面に配置されるベクターの集合体
  Particlesの動きに力を加える
  */
  
  PVector[] vectors;
  int cols, rows;
  float inc = 0.1;
  float zoff = 0;
  int scl;
  
  FlowField(int res) {
    scl = res;
    cols = floor(width / res) + 1;
    rows = floor(height / res) + 1;
    vectors = new PVector[cols * rows];
  }

  void update() {
    // flowfield状態を定義
    // フレームごとに呼び出される

    float xoff = 0;
    for (int y = 0; y < rows; y++) { 
      float yoff = 0;
      for (int x = 0; x < cols; x++) {
        float angle = noise(xoff, yoff, zoff) * radians(180); // ここで各ベクターの角度を定義
        
        PVector v = PVector.fromAngle(angle); // Flowfieldを構成するベクター
        v.setMag(1); // 各ベクターの大きさを定義
        int index = x + y * cols;
        vectors[index] = v;
       
        xoff += inc;
      }
      yoff += inc;
    }
    zoff += 0.004;
  }
}


public class Particle {
  /*
  ひとつの粒子
  動きはflowfieldのベクターから影響を受ける
  */

  PVector pos;
  PVector vel;
  PVector acc;
  PVector previousPos;
  float maxSpeed;
   
  Particle(PVector start, float maxspeed) {
    maxSpeed = maxspeed;
    pos = start;
    vel = new PVector(0, 0);
    acc = new PVector(0, 0);
    previousPos = pos.copy();
  }

  void run() {
    update();
    edges();
    show();
  }

  void update() {
    pos.add(vel);
    vel.limit(maxSpeed);
    vel.add(acc);
    acc.mult(0);
  }

  void applyForce(PVector force) {
    acc.add(force); 
  }

  void show() {
    // 描画処理

    stroke(10, 2); // ここで線の色調整
    strokeWeight(1); // ここで線の太さ調整
    line(pos.x, pos.y, previousPos.x, previousPos.y);
    updatePreviousPos();
  }

  void edges() {
    // 画面端の処理

    if (pos.x > width) {
      pos.x = 0;
      updatePreviousPos();
    }
    if (pos.x < 0) {
      pos.x = width;    
      updatePreviousPos();
    }
    if (pos.y > height) {
      pos.y = 0;
      updatePreviousPos();
    }
    if (pos.y < 0) {
      pos.y = height;
      updatePreviousPos();
    }
  }

  void updatePreviousPos() {
    this.previousPos.x = pos.x;
    this.previousPos.y = pos.y;
  }

  void follow(FlowField flowfield) {
    // Flowfieldから受ける力を適用

    int x = floor(pos.x / flowfield.scl);
    int y = floor(pos.y / flowfield.scl);
    int index = x + y * flowfield.cols;
    
    PVector force = flowfield.vectors[index];
    applyForce(force);
  }
}

Discussion

私はDaniel ShiffmanのYoutubeチャンネルThe Coding Trainが大好きです。
今回はその動画の一つである、Perlin Noise Flow Fieldの実装コードを参考にしました。

今回のProcessingコードは、FlowfieldParticleのふたつのクラスを持ちます。
おかげで少し長いコードになってますが、簡単に応用できるが良いところです。

Flowfieldクラスは、画面に格子状に配置されるベクターを定義しています。 このパラメータを操作すれば、自由にモデルの流れを制御することができます。

Particleクラスは、Flowfieldの流れの影響を受ける一つ一つの粒子を定義しています。 粒子の数や描画方法、大きさや色を自由に操作することができます。

今回は元のコード大して変わらない簡単な定義でモデルを走らせましたが、工夫次第でいくらでも遊べそうな予感に、胸が膨らみます。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference

Coding Challenge #24: Perlin Noise Flow Field - Daniel Shiffman

Coding Challenge #24: Perlin Noise Flow Field - Daniel Shiffman - GitHub


Generative Art #9 - Processing作品

こんにちは、@bioerrorlogです。

ProcessingによるGenerative Art作品
9作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

フル解像度/その他出力パターンはこちら:
Generative_9 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

float _noiseScale;


void setup(){
    _noiseScale = 0.005;

    size(1920, 1080);
    background(255);
}


void draw(){
    for (int i = 0; i < 10000; i++){
        float x = random(width);
        float y = random(height);
        float noiseFactor = noise(x*_noiseScale, y*_noiseScale); // 2次元noise
        float lineLength = noiseFactor * 40; // ここで線の長さを調整
        
        pushMatrix();
            translate(x, y);
            rotate(noiseFactor * radians(180)); // ここで線の流れ具合を調整
            stroke(0, 2);
            strokeWeight(1);
            line(0, 0, lineLength, lineLength);
        popMatrix();
    }
}


void keyPressed(){
    /*
    ENTERキー押下: 画像を保存する
    BACKSPACEキー押下: リセット
    */
    
    if (keyCode == ENTER){
        saveFrame("generative_9_####.png");
    }
    if (keyCode == BACKSPACE){
        setup();
    }
}

Discussion

前回、二次元パーリンノイズを用いて写真を油絵のように出力してみましたが、 今回は単純に二次元パーリンノイズの流れに薄く線を重ねてみました。

一つ一つの線の長さやノイズにかけ合わせる数の大きさによって、出力の雰囲気が大きく異なるのも面白いものです。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Generative Art #8 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
8作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20191218081834p:plain f:id:BioErrorLog:20191218081854p:plain f:id:BioErrorLog:20191218081914p:plain f:id:BioErrorLog:20191218081935p:plain

フル解像度/その他出力パターンはこちら:
Generative_8 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

PImage _img;
float _noiseScale;


void setup(){
    _img = loadImage("img.jpg"); // 画像はスケッチフォルダに入れておく
    _noiseScale = 0.005;

    size(1478, 1108); // 今回読み込む画像と同じサイズで
    background(255);
}


void draw(){
    /*
    画像から取得したピクセルカラー情報を用いて線を描く
    線を引く方向はxy情報に基づいた2次元noiseで流す
    */

    for (int i = 0; i < 100; i++){
        float x = random(_img.width);
        float y = random(_img.height);
        color col = _img.get(int(x), int(y)); // 画像のピクセルカラー情報を取得
        float noiseFactor = noise(x*_noiseScale, y*_noiseScale); // 2次元noise
        float lineLength = noiseFactor * 40; // ここで線の長さを調整
        
        pushMatrix();
            translate(x, y);
            rotate(noiseFactor * radians(180)); // ここで線の流れ具合を調整
            stroke(col);
            strokeWeight(8);
            line(0, 0, lineLength, lineLength);
        popMatrix();
    }

    // Frame毎に画像を保存
    saveFrame("img/generative_8_####.png");
}

Discussion

写真をもとに、油絵のような雰囲気の出力を出しました。

写真のピクセルカラー情報をもとに、2次元ノイズで作った流れで線を描きます。

線の形状にも工夫を加えれば、ほかの様々な画風も演出できそうで面白いです。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference

Noise flow paintというやり方をこちらで知りました。
Noise flow field painter (Deconstructed) - OpenProcessing

画像データの扱い方はこちらを参考にしました。
10.7: Painting with Pixels - Processing Tutorial by The Coding Train


Generative Art #7 - Processing作品

おはよう。@bioerrorlogです。

ProcessingによるGenerative Art作品
7作目を記録します。

自作品まとめはこちら: www.bioerrorlog.work

Output

f:id:BioErrorLog:20191217074804p:plain f:id:BioErrorLog:20191217074822p:plain f:id:BioErrorLog:20191217074913p:plain f:id:BioErrorLog:20191217074947p:plain

フル解像度/その他出力パターンはこちら:
Generative_7 - BioErrorLog - pixiv


Material

使用言語: Processing 3.5.3

Processingというプログラミング言語に聞き覚えがない、という方は是非こちらをご参考ください:
www.bioerrorlog.work


Source Code

GitHubはこちら

Cell[][] _cellArray;
int _cellSize = 2; // Cellの大きさ(pixel)
int _numX, _numY; // ディスプレイのCell格子


void setup() { 
  size(1920, 1080);
  _numX = floor(width/_cellSize);
  _numY = floor(height/_cellSize);
  restart();
} 


void restart() {
  // _cellArrayに画面分のCellを入れる
  _cellArray = new Cell[_numX][_numY];    
  for (int x = 0; x<_numX; x++) {
    for (int y = 0; y<_numY; y++) {    
      Cell newCell = new Cell(x, y);  
      _cellArray[x][y] = newCell;      
    }               
  }                 

  
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {  
      
      int above = y-1;        
      int below = y+1;        
      int left = x-1;         
      int right = x+1;            
      
      // 画面端の処理
      if (above < 0) { above = _numY-1; }    
      if (below == _numY) { below = 0; } 
      if (left < 0) { left = _numX-1; }  
      if (right == _numX) { right = 0; } 

     _cellArray[x][y].addNeighbour(_cellArray[left][above]);    
     _cellArray[x][y].addNeighbour(_cellArray[left][y]);        
     _cellArray[x][y].addNeighbour(_cellArray[left][below]);    
     _cellArray[x][y].addNeighbour(_cellArray[x][below]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][below]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][y]);   
     _cellArray[x][y].addNeighbour(_cellArray[right][above]);   
     _cellArray[x][y].addNeighbour(_cellArray[x][above]);       
    }
  }
}


void draw() {
  background(200);
                    
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {
     _cellArray[x][y].calcNextState();
    }
  }
                        
  translate(_cellSize/2, _cellSize/2);     
                        
  for (int x = 0; x < _numX; x++) {
    for (int y = 0; y < _numY; y++) {
     _cellArray[x][y].drawMe();
    }
  }

  saveFrame("frames/generative_7_#####.png");
}


class Cell {
  float x, y;
  float state;         
  float nextState;  
  float lastState = 0; 
  Cell[] neighbours;
  
  Cell(float ex, float why) {
    x = ex * _cellSize;
    y = why * _cellSize;
    
    // Cellの初期状態を定義
    // 確率で50、それ以外0
    if(random(100) < 1){
      nextState = 50; 
    }else{
      nextState = 0;
    }

    state = nextState;
    neighbours = new Cell[0];
  }
  
  void addNeighbour(Cell cell) {
    neighbours = (Cell[])append(neighbours, cell); 
  }
  
  void calcNextState() {
    /*
    1. もし隣接するセルの状態の平均が0であるなら状態は30に
    2. そうでなければ、新しい状態=現在の状態+隣接セルの状態の平均ー前の状態の値
    3. もし新しい状態が255を超えたら255にし、
    4. もし新しい状態が0以下ならそれを0にする
    */
                
    float total = 0;              
    for (int i=0; i < neighbours.length; i++) { 
       total += neighbours[i].state;        
    }                   
    float average = int(total/8);
                
    if (average == 0) {
      nextState = 30; // 1.
    } else {
      nextState = state + average;
      if (lastState > 0) { nextState -= lastState; } // 2.
      if (nextState > 255) { nextState = 255; } // 3.
      else if (nextState < 0) { nextState = 0; } // 4.
    }
 
    lastState = state;  
  }
  
  void drawMe() {
    state = nextState;
    noStroke();
    fill(255 - state); // 白黒反転
    rect(x, y, _cellSize, _cellSize);
  }
}

Discussion

前回と同様、マット・ピアソン「ジェネラティブ・アート―Processingによる実践ガイド」7章で紹介される、セルオートマトンのケーススタディを改変して作成。

セルオートマトンの規則を次のように定めてみました。

  • もし隣接するセルの状態の平均が0であるなら状態は30に
  • そうでなければ、新しい状態=現在の状態+隣接セルの状態の平均ー前の状態の値
  • もし新しい状態が255を超えたら255にし、
  • もし新しい状態が0以下ならそれを0にする

モデル全体がまるで生きているような、一定の拍動を打つのが面白いところです。
静止画として切り出すと、各タイミングで全く違った絵になるのも不思議なもの。

セルオートマトンの奥深さの一端が見れました。


See also

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

Reference