とある科学の備忘録

とある科学の備忘録

CやPythonのプログラミング、Arduino等を使った電子工作をメインに書いています。また、木製CNCやドローンの自作製作記も更新中です。たまに機械学習とかもやってます。

【動画編集】無料で動画編集を始めようという人へ➀ 必要なもの

これから動画編集を始めよう、という人に向けたアドバイスを書いていきます。私自身、まだ動画編集は初心者なので、間違ったことを書いていたらごめんなさい

この記事に書いてあるソフトウェア、素材はすべて無料で手に入るものです。有難いーー!!

必要な機材

まずはハードウェアの方から見ていきます。
結論から言えばスマホとPCがあればOKです。

名称 備考
カメラ 最近のカメラは性能がいいのでスマホカメラで十分!一眼レフなどがあればbetter
マイク ASMRでもしない限りスマホのマイクで十分!個人的にはスマホマイクの中ではiPhone X系統のマイクが気に入っています。ちょっとくらいの爆音(ライブを遠くから見てるくらい)だと音割れがほとんど無かった(気がする)
編集機材 スマホのVivaVideoというアプリで編集している知人のYouTuberがいましたが、5~10分くらいの動画でも半日くらいかかると言っていました。PCを買って編集することを強くお勧めします。スペックは、メモリ8GB、SSD256~512GBが最低条件です。下の項目ではWindowsの設定で進めていますが、MacでもBootCampでWindowsを起動できるので問題ないはずです。
記憶媒体 外付けHDD、外付けSSDです。撮影したメディアを保存する媒体ですが、別になくてもPCの内部ストレージに保存すれば編集はできるので、急いで買う必要はないと思います(PCの内部ストレージが素材でいっぱいになった時に買えばいい)。


必要なソフトウェア

機材がそろったところで、Windows10 のパソコンで動画編集をしようと思ったら、何をPCに入れればいいのか?

以下のソフトウェアが必要になります。

ソフトウェア名 使用用途 どれくらい必要か
AviUtl 万能の無料動画編集ソフト 絶対必要。AviUtl本体+拡張編集プラグイン+外部スクリプト色々の3点セットが必須!
Davinch Resolve カラーグレーディングに特化した動画編集ソフト 無くてもOK。あればよりきれいな映像が作れる
Blender 3DCGソフト 3DCGを作りたければ必須
Reaper 無料音声編集ソフト(DAW 音にこだわる場合はあったほうが良い
ffmpeg 動画と音声を記録・変換する あると編集作業が楽になる

つまり、AviUtlは絶対必要、それ以外はあったら便利というイメージです。
それぞれのソフトウェアを順番に見ていきます。

AviUtl

f:id:pythonjacascript:20200808175209j:plain
動画編集の90%をAviUtlで行います。AdobeのAfterEffectsやPremire Proを知っていれば、その2つを足して2で割ったような感じのソフトウェアです。

編集を行うときに「拡張編集」と有志が作ってくれた「外部スクリプトを導入しなければまともな編集ができません。そこら辺の導入方法はいろんなサイトに転がっているので調べてください。

入れておくべきプラグインスクリプトを書いておきます。

プラグイン 説明 備考
入力プラグイン L-Smash Works File Reader 読み込むことができる拡張子を増やすことができる
入力プラグイン DirectShow File Reader 読み込むことができる拡張子を増やすことができる。L-Smash WorksではFPSが微妙(23.45fpsみたいな感じ)な動画が音ズレする問題がありますが、DirectShowでは音ズレしません
出力プラグイン 拡張x264出力 mp4で書きだすために必須
入力プラグイン 拡張編集RAMプレビュー 場合によってはAVI出力したほうが早い可能性もあるので必須ではないが、あったほうが便利だと思う
外部スクリプト イージング 動きに緩急をつけるために必須!!絶対必要!
外部スクリプト 有志の方々スクリプト] あったほうが絶対に良い!!

大量の外部スクリプトがある中でも、個人的なおすすめはここらへんです:

  • MultiSlicer
  • MorphDots
  • SimpleTube
  • DelayMove
  • GetColor-V2
  • PixelSort
  • ParallelCamera
  • TA_ssd
  • ANM1, ANM2
  • レンズ補正もどき


過去作品(宣伝)


因みにこれはAviUtlとフリー素材だけで作成しています。


Davinch Resolve

f:id:pythonjacascript:20200808175435j:plain

Davinch Resolve は「カラーグレーディング(Color Grading)」や「カラーコレクティング(Color Correcting)」に特化したソフトです。「カラーグレーディング」とは、映像の色彩を補正する技術です。

参考リンク:Color Gradingってこんなものだよ、という事
www.youtube.com


このように映像に色身を加えることで、映像の印象を変えたりインパクトを強めたりすることができます。AviUtlでもフィルタ効果の「色調補正」と「拡張色調補正」で似たようなことができますが、Davinch Resolveの方が圧倒的に高機能です。Cinematic Vlogを作りたいとか、自主製作映画を撮りたいという場合はDavinch Resolveが必要だと思います。

しかし、Davinch ResolveでMotion Graphicsをするなどといった話はあまり聞かないので、全体的な構造をAviUtlで作り、Davinch Resolveでそのあとカラーグレーディングを行うという手法が一般的の様です。


Reaper

f:id:pythonjacascript:20200808182119j:plain
Reaperは俗にいう「DAWDigital Audio Workstation」ソフトです。音楽の編集、エフェクト追加、ミキシングなどができます。AviUtlは音声編集がほとんどできません(パンと音量調整くらい)。だから、「外で撮影した音にノイズが入っているからノイズ除去したい」、「YouTubeで歌ってみた動画や弾いてみた動画を出したい」という場合は、音声の編集をReaperで行い、映像の編集をAviUtlで行うという手順が必要だと思います。

DAWソフトはほかにもAudacityやGarage Band(Macのみ)などがありますが、Reaperが一番UIが見やすい気がします。気のせいかもしれないですが。


Blender

3DCGソフトです。無料ソフトですが、ジュラシックパークなど有名な映画作品でもVFX用のソフトウェアとして使われています。

私は、昔「シンゴジラ」のパロディを作っていた時にビルの破壊シーンが必要で、Blenderでシミュレーションして作っていました。
f:id:pythonjacascript:20200808175949j:plain
f:id:pythonjacascript:20200808175947j:plain
レンダリングに丸一日かかったぜ!!)


FFmpeg

以上のソフトウェアで大抵の動画編集はできます。ではFFmpegは何をするのかというと「編集作業の効率化」を行います。
例えば、ある動画ファイルを音声ファイルに変換したい時、コマンドプロンプト

$ ffmpeg -i input.mp4 output.wav

と打ち込むだけで変換できます。コマンド式などで、Python等のプログラミング言語を使って一括処理できます。例えば、以下のプログラムは.pyファイルがあるフォルダ内の動画ファイル(mp4形式)のFPSを30に一括変換するプログラムです。

import subprocess
import glob
import re
import os

def main():
    os.makedirs("30fps", exist_ok=True)
    fname_list = glob.glob('*.mp4')

    print(f"filename list = {fname_list}")
    for fname in fname_list:
        print("converting ... " + fname)
        out_name = "30fps/30fps_" + fname

        command = f'ffmpeg -i "{fname}"  -filter:v fps=fps=30 "{out_name}"'
        subprocess.call(command, shell=True) #FPS変換
main()

 
プログラムが分からないという人は、別にFFmpegが無くても問題ありません。あったらちょっと便利、という程度です。



フリー素材を活用する!

動画編

YouTube

YouTubeはフリー素材の宝庫です。youtube-dl 等を利用してダウンロードして使った方が編集の効率化にもなるしいいと思います。

例えば、YouTubeには以下の様なフリー素材があります。

種類 リンク
ライトリーク https://www.youtube.com/watch?v=5Ehd4cFEvnQ&list=PLuQeubuanE4gLVm2OFj9JH7tT3ie-6tKl
グリッチ https://www.youtube.com/watch?v=tRNZPYTMWyg&list=PLVdLN4Yhy71fl395oQBm_LgspzLW3cCLI
かっこいい背景 https://www.youtube.com/watch?v=nfDobLtkacg&list=PLuP9GymJJRD4BqXSNG9ZuDxLw0m_l9ASY
インクを落とした時 https://www.youtube.com/watch?v=9GRmK4UXjys&list=PLtccKDoSQZZONdtSOh0nWfJa1UNfAXykA
ブラシでシュッてする https://www.youtube.com/watch?v=aa30IkDB5BM
トランシジョン https://www.youtube.com/watch?v=Tx3eVmDoIBI&list=PLKNfuJ73yZ8HkMrr3fO9-7NbbWS8pjbTD
爆発 https://www.youtube.com/watch?v=SF9eRLQsQFc&list=PL7MTQD87g5C7ubj6pJ5WntuXCIWidxpoo
色々詰め合わせ https://www.youtube.com/watch?v=PtsrQb7_RLo&list=PLsK2kC4aOfPPwIF7oBc5qZZTn92vg0zc-

これでもまだ氷山の一角です。大量にあるので、初心者のうちはよっぽどのことが無い限りFilmonaの有料ソフトを買って素材を手に入れる等の必要はないと思います。


fstock

そして、最近fstockというリキッドアニメをメインで扱っている素晴らしいサイトに出会うことができました。
f-stock.net


参考:https://www.youtube.com/watch?v=yNk24YaMY2k
めっちゃ参考になりましたm(__)m!


画像偏

画像に関してはニコニコモンズが一番便利だと思っています。
commons.nicovideo.jp

検索すれば色々なものがそろっています。


音楽

音楽もやはり、YouTubeが一枚買っている気がします。今のYouTuberが使っているBGMは大抵YouTubeで手に入ります。
https://youtu.be/-sd_ZWrFJ9s?list=PL2vyqKNLXiUpl8AoOKI3rMrEzLGx_-1m2

https://youtu.be/AivyFZXT2ek?list=PLIILL6veL783kKkdiIybbxARNY9bAVQYe



全て無料で著作権フリーの作曲サイトもあります。AIが作曲しているらしく、カテゴリーを入力するとそれに合ったBGMを選んでくれます。
evokemusic.ai
このサイトがすごいのはパートごとにダウンロードできることです。曲全体(Mixされた完成品)は勿論ダウンロードできますが、ドラムだけダウンロードとかベースだけDL、という事も可能です!!



効果音

スマホ+Reaperでノイズ除去(+EQ)でもそれっぽい音を作ることができますが、労力もかかるし音撮りしている時に周囲から変な目で見られるので、普段は以下の様なサイトにお世話になっています。
soundeffect-lab.info

taira-komori.jpn.org

【C++/OpenCV】OpenCVでピクセルソート!

ピクセルソートとは

AfterEffectとかAviUtlの中の数あるエフェクトの中でも気に入っているのがPixelSortです。(AviUtlの場合はAodaruma様の外部スクリプトを導入する必要があります)

f:id:pythonjacascript:20200808155043g:plain
(AviUtlでのPixelSortの例)


その名の通り、画像の「ピクセル」を「ソート(並び替え)」するエフェクトです。並び替え順序は明るい順とか暗い順とかいろいろあるみたいです。そして、並び替えるピクセルも、明るい部分だけソートする、暗い部分だけソートする、という事ができます。

今回はこのPixelSortエフェクトをC++OpenCVを使って実装してみます。

プログラム

# include <opencv2/opencv.hpp>
# include <opencv2/highgui.hpp>
#include <initializer_list>
#include <iostream>
#include <vector>
#include <algorithm>
#include <assert.h>

#define ESCAPE_KEY 27

void do_nothing(int size, void*) {}

cv::Mat img;

int mid_value = 100; //閾値0 ~ 256
int range = 0;

int width = 0;
int height = 0;



cv::Vec3b getColor(cv::Mat *img, int x, int y) {
  cv::Vec3b *ptr = (*img).ptr<cv::Vec3b>(y);
  cv::Vec3b color = ptr[x];
  return color;
}

int getV(cv::Vec3b color) {
  return std::max({ color[0], color[1], color[2] });
}



void getStartEndBlackPosY(int x, int *start, int *end) {
  assert(x < 0);
  assert(y < 0);
  assert(x > width - 1);
  assert(y > height - 1);
  int y = *start;

  *end = y+1;

  cv::Vec3b color = getColor(&img, x, y);
  int v = getV(color);

  while (v < mid_value + range) {
    (*start)++;
    if ((*start) >= height - 1) {
      *end = height - 1;
      return;
    }

    v = getV(getColor(&img, x, *start));
  }

  while (v > mid_value - range) {
    (*end)++;
    if ((*end) >= height - 1) {return;}
    v = getV(getColor(&img, x, *end));
  }
}


void getStartEndRangePosY(int x, int *start, int *end) {
  assert(x < 0);
  assert(y < 0);
  assert(x > width - 1);
  assert(y > height - 1);
  int y = *start;

  *end = y + 1;

  cv::Vec3b color = getColor(&img, x, y);
  int v = getV(color);

  while (v < mid_value - range ||  mid_value + range < v) {
    (*start)++;
    if ((*start) >= height - 1) {
      *end = height - 1;
      return;
    }

    v = getV(getColor(&img, x, *start));
  }

  while (mid_value - range <= v && v <= mid_value + range) {
    (*end)++;
    if ((*end) >= height - 1) { return; }
    v = getV(getColor(&img, x, *end));
  }
}


void getStartEndWhitePosY(int x, int *start, int *end) {
  assert(x < 0);
  assert(y < 0);
  assert(x > width - 1);
  assert(y > height - 1);
  int y = *start;

  *end = y + 1;

  cv::Vec3b color = getColor(&img, x, y);
  int v = getV(color);

  while (v > mid_value-range) {
    (*start)++;
    if ((*start) >= height - 1) {
      *end = height - 1;
      return;
    }

    v = getV(getColor(&img, x, *start));
  }

  while (v < mid_value+range) {
    (*end)++;
    if ((*end) >= height - 1) { return; }
    v = getV(getColor(&img, x, *end));
  }
}


void pixelSort() {
  int column = 0;
  while (column < width - 1) {
    int x = column;
    int start = 0;
    int end = 0;

    while (end < height - 1) {
      getStartEndRangePosY(x, &start, &end);   // mid_value  - range < brightness < mid_value + rangeのピクセルをソートする
      //getStartEndWhitePosY(x, &start, &end); // mid_value < brightness のピクセルをソートする(mid_valueよりも明るい部分をソート)
      //getStartEndBlackPosY(x, &start, &end); // brightness < mid_valueのピクセルをソートする(mid_valueよりも暗い部分をソート)

      if (start >= height - 1) break;

      int sortLength = end - start;

      std::vector<cv::Vec3b> sorting;

      for (int i = 0; i < sortLength; i++) {
        cv::Vec3b* ptr = img.ptr<cv::Vec3b>(start + i);
        sorting.push_back(ptr[x]);
      }


      //std::sort(sorting.begin(), sorting.end(), [](const cv::Vec3b& c1, const cv::Vec3b& c2) {return (c1[0] * 65025 + c1[1] * 255 + c1[2]) < (c2[0] * 65025 + c2[1] * 255 + c2[2]); });
      std::sort(sorting.begin(), sorting.end(), [](const cv::Vec3b& c1, const cv::Vec3b& c2) {return (c1[0] + c1[1] + c1[2]) < (c2[0] + c2[1] + c2[2]); });

      for (int i = 0; i < sortLength; i++) {
        cv::Vec3b* ptr = img.ptr<cv::Vec3b>(start + i);
        ptr[x] = sorting[i];
      }

      start = end + 1;
    }
    column++;
  }
}



void main() {
  cv::Mat img_before = cv::imread("C:/Users/Owner/Desktop/Cpp_Source/AUL_project/Effects/sample_images/fulHD.jpg");
  width = img_before.cols;
  height = img_before.rows;

  while (1) {
    img = img_before.clone();
    pixelSort();


    cv::resize(img, img, cv::Size(int(width*0.5), int(height*0.5)));
    cv::imshow("test", img);
    cv::createTrackbar("mid_value", "test", &mid_value, 256, do_nothing);
    cv::createTrackbar("range", "test", &range, 256, do_nothing);
    std::cout << "threshold = " << mid_value <<", " << range << "\n";


    if (cv::waitKey(1) == ESCAPE_KEY) {break;}
  }
}



結果

スライダーの値をいじると、下の画像の様にピクセルソートされます。
f:id:pythonjacascript:20200808161540j:plain
今回作ったPixelSortはmid_valueとrangeの2つのパラメータを持っていて、スライダーでそれらの値を調節できるようにしました。
(参考→【C++】OpenCV標準のスライダーを使用する - とある科学の備忘録

因みにですが元画像はこちらです。
f:id:pythonjacascript:20200808162048j:plain
サイズは1920*1080です。

このサイズを上のC++プログラムでPixelSortすると画像処理に約0.5~2秒くらいかかります。(メモリー8GB、i7-6500U)
まぁ十分なスピードの様な気もします。



PixelSort関数中に

getStartEndRangePosY(x, &start, &end);   // mid_value  - range < brightness < mid_value + rangeのピクセルをソートする
//getStartEndWhitePosY(x, &start, &end); // mid_value < brightness のピクセルをソートする(mid_valueよりも明るい部分をソート)
//getStartEndBlackPosY(x, &start, &end); // brightness < mid_valueのピクセルをソートする(mid_valueよりも暗い部分をソート)

の3行がありますが、このうちどの1行をコメントアウトするかでソートするピクセルの取捨選択方法が変わります。


参考にしたプログラム

github.com

【C++/OpenGL】インスタンス化を用いてパーティクル出力

にゃんぱすー。お久しぶりです。期末テストが終わって夏休みに入ったのでブログを再開しようかなぁと考えているところです。


今回作るのはこちら!
f:id:pythonjacascript:20200808134507g:plain

では、早速プログラムから。

プログラム

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <iostream>

#define MAX_PARTICLE_N 5000
#define PARTICLE_LIFE_SECONDS 6.0
#define DEFAULT_POS_X -0.5
#define DEFAULT_POS_Y -0.5
#define DEFAULT_POS_Z 0.1
#define DEFAULT_VEL_X 0.2
#define DEFAULT_VEL_Y 0.15
#define DEFAULT_VEL_Z 0.2
#define SPREAD_RATE   0.1
#define DEFAULT_SIZE  1.0


GLint makeShader() {
  const char* vertex_shader =
    "#version 400\n"
    "layout(location = 0) in vec3 billbords;\n"
    "layout(location = 1) in vec4 position;\n"
    "layout(location = 2) in vec4 color;\n"
    "out vec4 outColor;\n"
    "void main(void) {\n"
    "outColor = color;\n"
    "gl_Position = vec4(position.xyz + billbords*position.w, 1.0f);\n"
    "}\n";

  const char* fragment_shader =
    "#version 400\n"
    "in vec4 outColor; \n"
    "out vec4 outFragmentColor; \n"
    "void main(void) {\n"
    "outFragmentColor = outColor; \n"
    "}\n";

  GLuint vs, fs, shader_programme;

  vs = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vs, 1, &vertex_shader, NULL);
  glCompileShader(vs);
  //GLint success = 0;
  //glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
  //if (success == GL_FALSE) std::cout << "頂点シェーダー作成に失敗!!";

  fs = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fs, 1, &fragment_shader, NULL);
  glCompileShader(fs);
  //GLint success = 0;
  //glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
  //if (success == GL_FALSE) std::cout << "フラグメントシェーダー作成に失敗!!";

  shader_programme = glCreateProgram();
  glAttachShader(shader_programme, fs);
  glAttachShader(shader_programme, vs);
  glLinkProgram(shader_programme);

  return shader_programme;
}



// CPUでパーティクルを作成する。それぞれの粒子一つ一つがこの値を持っている。
struct Particle {
  float pos[3];
  float speed[3];
  float color[4];
  float size;
  float life; // 粒子の寿命。精製された瞬間はPARTICLE_LIFE_SECONDSが代入され、時間がたつと徐々に減少して、負の数になっている場合は描画されない。
  float cameradistance; // 中心座標からの距離の二乗。これでソートする

  bool operator<(const Particle& that) const {
    // Sort in reverse order : far particles drawn first.
    return this->cameradistance > that.cameradistance;
  }
};


Particle ParticlesContainer[MAX_PARTICLE_N];
int LastUsedParticle = 0;

// life < 0.となっている(つまり描画されていない)パーティクルを探す。
// この後そのパーティクルを初期化して復活させるため
int FindUnusedParticle() {
  for (int i = LastUsedParticle; i < MAX_PARTICLE_N; i++) {
    if (ParticlesContainer[i].life < 0) {
      LastUsedParticle = i;
      return i;
    }
  }
  for (int i = 0; i < LastUsedParticle; i++) {
    if (ParticlesContainer[i].life < 0) {
      LastUsedParticle = i;
      return i;
    }
  }

  return 0; // All particles are taken, override the first one
}


void SortParticles() {
  std::sort(&ParticlesContainer[0], &ParticlesContainer[MAX_PARTICLE_N]);
}


int main() {
  GLFWwindow *window = NULL;
  const GLubyte *renderer;
  const GLubyte *version;
  
  if (!glfwInit()) {
    fprintf(stderr, "ERROR: could not start GLFW3\n");
    return 1;
  }

  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);

  window = glfwCreateWindow(640, 480, "Particle", NULL, NULL);
  if (!window) {
    fprintf(stderr, "ERROR: could not open window with GLFW3\n");
    glfwTerminate();
    return 1;
  }

  // 今からこのウィンドウについての設定を行うよ~みたいな意味
  glfwMakeContextCurrent(window);

  //GLEWまたはGLADを初期化する(Glewを使うかGladを使うかでの変更点はここだけ)

  // GLEWの初期化を行う(GLADを使う場合は下の二行はコメントアウト)
  glewExperimental = GL_TRUE;
  glewInit();

  // GLADを使う場合は下の二行をコメントアウトしてね
  // gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
  // glfwSwapInterval(1);


  renderer = glGetString(GL_RENDERER);
  version = glGetString(GL_VERSION);
  printf("Renderer: %s\n", renderer);
  printf("OpenGL version supported %s\n", version);

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);



  static GLfloat* g_particule_position = new GLfloat[MAX_PARTICLE_N * 4];
  static GLfloat* g_particule_color = new GLfloat[MAX_PARTICLE_N * 4];


  for (int i = 0; i < MAX_PARTICLE_N; i++) {
    ParticlesContainer[i].life = -1.0f;
    ParticlesContainer[i].cameradistance = -1.0f;
  }

  // それぞれのパーティクルの形状を表している。
  // インスタンス化のお陰で、パーティクル全ての形状をこの12変数だけで表すことができる
  // (インスタンス化をしない場合は 12 x 粒子数 だけデータ必要になる)
  static const GLfloat g_vertex_buffer_data[] = {
     -0.01f, -0.01f, 0.0f,
      0.01f, -0.01f, 0.0f,
     -0.01f,  0.01f, 0.0f,
      0.01f,  0.01f, 0.0f,
  };


  GLint shader = makeShader();
  GLuint vao;
  GLuint vertex_vbo, color_vbo, shape_vbo;

  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  // 粒子形状のVBOを作成
  glGenBuffers(1, &shape_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, shape_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);


  // 頂点座標のVBOを作成  
  glGenBuffers(1, &vertex_vbo); //バッファを作成
  glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo); //以下よりvertex_vboでバインドされているバッファが処理される
  glBufferData(GL_ARRAY_BUFFER, sizeof(g_particule_position), g_particule_position, GL_STATIC_DRAW); //実データを格納

  // 色のVBOを作成
  glGenBuffers(1, &color_vbo); //バッファを作成
  glBindBuffer(GL_ARRAY_BUFFER, color_vbo); //以下よりcolor_vboでバインドされているバッファが処理される
  glBufferData(GL_ARRAY_BUFFER, sizeof(g_particule_color), g_particule_color, GL_STATIC_DRAW); //実データを格納



  double lastTime = glfwGetTime();

  while (!glfwWindowShouldClose(window)) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    double currentTime = glfwGetTime();
    double delta = currentTime - lastTime;
    lastTime = currentTime;

    //1msあたり10個の粒子を新しく作る
    int newparticles = (int)(delta*10000.0);
    if (newparticles > (int)(0.016f*10000.0)) //新しく作る粒子の数が巨大になりすぎないように制限
      newparticles = (int)(0.016f*10000.0);

    for (int i = 0; i < newparticles; i++) {
      //まだ作られていない(またはlife < 0で死亡した)パーティクルを探して初期化する

      int particleIndex = FindUnusedParticle(); 
      ParticlesContainer[particleIndex].life = PARTICLE_LIFE_SECONDS;
      ParticlesContainer[particleIndex].pos[0] = DEFAULT_POS_X;
      ParticlesContainer[particleIndex].pos[1] = DEFAULT_POS_Y;
      ParticlesContainer[particleIndex].pos[2] = DEFAULT_POS_Z;
      ParticlesContainer[particleIndex].pos[3] = DEFAULT_SIZE;

      ParticlesContainer[particleIndex].speed[0] = DEFAULT_VEL_X + (rand() % 2000 - 1000.0f)* SPREAD_RATE / 1000.0f;
      ParticlesContainer[particleIndex].speed[1] = DEFAULT_VEL_Y + (rand() % 2000 - 1000.0f)* SPREAD_RATE / 1000.0f;
      ParticlesContainer[particleIndex].speed[2] = DEFAULT_VEL_Z + (rand() % 2000 - 1000.0f)* SPREAD_RATE / 1000.0f;

      ParticlesContainer[particleIndex].color[0] = rand() % 10000 / 10000.0;
      ParticlesContainer[particleIndex].color[1] = rand() % 10000 / 10000.0;
      ParticlesContainer[particleIndex].color[2] = rand() % 10000 / 10000.0;
      ParticlesContainer[particleIndex].color[3] = 1.0; //rand() % 10000 / 10000.0;

      ParticlesContainer[particleIndex].size = (rand() % 1000) / 20000.0f + DEFAULT_SIZE;
    }


    //パーティクルをCPUでシミュレーションする
    int ParticlesCount = 0;
    for (int i = 0; i < MAX_PARTICLE_N; i++) {
      Particle& p = ParticlesContainer[i];

      p.life -= delta;
      if (p.life > 0.0f) {
        //等速直線運動
        p.pos[0] += p.speed[0] * (float)delta;
        p.pos[1] += p.speed[1] * (float)delta;
        p.pos[2] += p.speed[2] * (float)delta;

        p.cameradistance = (p.pos[0])*(p.pos[0]) + (p.pos[1])*(p.pos[1]) + (p.pos[2])*(p.pos[2]);

        // GPUに転送するバッファに値をコピーする
        g_particule_position[4 * ParticlesCount + 0] = p.pos[0];
        g_particule_position[4 * ParticlesCount + 1] = p.pos[1];
        g_particule_position[4 * ParticlesCount + 2] = p.pos[2];
        g_particule_position[4 * ParticlesCount + 3] = p.size;

        g_particule_color[4 * ParticlesCount + 0] = p.color[0];
        g_particule_color[4 * ParticlesCount + 1] = p.color[1];
        g_particule_color[4 * ParticlesCount + 2] = p.color[2];
        g_particule_color[4 * ParticlesCount + 3] = p.color[3];
      }else {
        //描画されないパーティクルはSortParticles()で最後のほうに並ぶようにする
        p.cameradistance = -1.0f;
      }

      ParticlesCount++;
    }

    SortParticles();


    printf("%d ",ParticlesCount);


    glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo);
    glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLE_N * 4 * sizeof(GLfloat), NULL, GL_STREAM_DRAW); // Buffer orphaning, a common way to improve streaming perf. See above link for details.
    glBufferSubData(GL_ARRAY_BUFFER, 0, ParticlesCount * sizeof(GLfloat) * 4, g_particule_position);

    glBindBuffer(GL_ARRAY_BUFFER, color_vbo);
    glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLE_N * 4 * sizeof(GLfloat), NULL, GL_STREAM_DRAW); // Buffer orphaning, a common way to improve streaming perf. See above link for details.
    glBufferSubData(GL_ARRAY_BUFFER, 0, ParticlesCount * sizeof(GLfloat) * 4, g_particule_color);


    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glUseProgram(shader);



    // 各種バッファにデータ転送
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, shape_vbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo);
    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, color_vbo);
    glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

    // https://qiita.com/y_UM4/items/585212422fea72faa889
    glVertexAttribDivisor(0, 0); // 頂点情報はすべての粒子に同じ値を使用し、分割しない → 第二引数は0
    glVertexAttribDivisor(1, 1); // 粒子の位置はすべての粒子で違うので4つごとにデータを区切る→ 第二引数は1
    glVertexAttribDivisor(2, 1); // 粒子の色はすべての粒子で違うので4つごとにデータを区切る→ 第二引数は1                               -> 1

    //描画!!
    //インスタンス化を使わない場合は for(i in ParticlesCount) : glDrawArrays(GL_TRIANGLE_STRIP, 0, 4), と書くこともできるが、
    // ドローコールが何回も呼び出されることで動作が遅くなるため、NG
    glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, ParticlesCount);

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);


    glfwPollEvents();
    glfwSwapBuffers(window);// Windowに描画内容を表示
  }

  glfwTerminate();//終了処理
  return 0;
}

  

実行結果

下のプログラムを実行すると以下の画像のようになります。
f:id:pythonjacascript:20200808134440j:plain



解説

上のプログラムの重要な部分の説明です。

インスタンス

上のGifアニメーションの様に同じ形状の大量のオブジェクトを描画したいという場合には「インスタンス」を使うと描画を効率化することができます。


今までのプログラムでは、描画の時にglDrawArrays()関数を使っていましたが、これでは大量のオブジェクトを描画するには、

//ParticlesCountは描画したい粒子の数
for(int i=0; i< ParticlesCount; i++){
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

のように書かなくてはいけません。glDrawArraysのようなDrawコールを行うとそのたびにCPUやGPUが色々な計算をするらしく(?)動作がもっさりしてしまうため、処理速度を上げようと思ったら、描画に必要なデータを一度にGPUに渡して、できるだけ少ない数のDrawコールで済むようにプログラムするのはいいみたいです。


ということで、インスタンス化の出番です。glDrawArraysの代わりにglDrawArraysInstanced関数を使うと、↑のプログラムは

//ParticlesCountは描画したい粒子の数
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4,ParticlesCount);

とドローコール一回で済むという事です。


データ共有によるメモリ削減(glVertexAttribDivisor)

上のプログラムは最大で5000個の粒子を描画します。それらの粒子の形状は全て同じなのに、前回までのプログラムの様にそれぞれの粒子について形状データを配列化して登録していたらメモリが無駄になってしまいます。


そこで、glVertexAttribDivisor関数を使うことで、全ての粒子に同じ値を適応することができます。

void glVertexAttribDivisor(	GLuint index, GLuint divisor);
// index = VBOのindex (glVertexAttribPointerの第一引数に指定した値)
// divisor == 0 --> index番目のVBOデータは、分割されずにすべての図形に共有される
// divisor == 1 --> index番目のVBOデータは、glVertexAttribPointerの第2引数で指定している頂点数で分割する。

以下の記事を参考にさせて頂きましたm(__)m
qiita.com


上のサンプルプログラムでは以下の部分が相当します、

glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, shape_vbo);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, vertex_vbo);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, color_vbo);
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

glVertexAttribDivisor(0, 0); // 頂点情報はすべての粒子に同じ値を使用し、分割しない → 第二引数は0
glVertexAttribDivisor(1, 1); // 粒子の位置はすべての粒子で違うので4つごとにデータを区切る→ 第二引数は1
glVertexAttribDivisor(2, 1); // 粒子の色はすべての粒子で違うので4つごとにデータを区切る→ 第二引数は1                 

//描画!!
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, ParticlesCount);



パーティクルのシミュレーション

それぞれの粒子には下の様な構造体を割り当てています。

struct Particle {
	float pos[3];
	float speed[3];
	float color[4];
	float size;
	float life; // 粒子の寿命。精製された瞬間はPARTICLE_LIFE_SECONDSが代入され、時間がたつと徐々に減少して、負の数になっている場合は描画されない。
	float cameradistance; // 中心座標からの距離の二乗。これでソートする

	bool operator<(const Particle& that) const {
		// Sort in reverse order : far particles drawn first.
		return this->cameradistance > that.cameradistance;
	}
};


そして、これらの値は以下の様にメインループの中でいじられています。

内容 説明
粒子の消滅 Particle.life変数が時間とともに徐々に減っていき、life < 0になったらcameradistance = -1.0にする。cameradistanceをもとにソートすると(SortParticles関数)後ろの方に追いやられ、描画されなくなる
粒子の生成 life < 0の粒子のうちいくつか(個数は経過時間によって決まる)を初期化する
粒子の初期化 lifeをPARTICLE_LIFE_SECONDSに設定し、頂点座標と粒子の色と移動速度をランダムの値と定数を用いて初期化する
粒子の移動 等速直線運動を行う(position = speed * dt)。運動後にcameradistanceを再計算する
粒子の描画 life > 0の粒子の位置と色をGPUに転送するためのバッファにコピーして描画