床井研究室

年末

年末です。今年も暮れていきます。ああ、どうしましょう。悲喜交々であります。今年も皆様方にはいろいろご迷惑をおかけしました。多くの方にご助力いただきました。ありがとうございました。まあ、継続中のご迷惑もまだあるんですけど。すみません、なんとかします。

髪の毛

卒研その他で髪の毛を扱う人向けに GPU を使う場合のサンプルプログラムを書きました。git で clone するか ZIP ファイルをダウンロードしてください。

とりあえず、こんな感じになります。まだ最終形態ではありません。てゆうか、最終形態へは自分で持ってってください。

頭髪シミュレーションの練習 from Kohe Tokoi on Vimeo.

関連する研究として Selle らの A Mass Spring Model for Hair Simulation や、Liu らの Fast simulation of mass-spring systems なんかがありそうですけど、詳しく調べてないので自分で調べておいてください。

データの作成

とりあえず、頭髪のデータを作ります。ちゃんとモデリングしたデータを使いたいところですけど、めんどくさいので適当な形をプログラムで作ります。

頭髪の初期形状

一本の髪の毛は、根元と先端を含んで、hairKnots 個の質量を持った節点 (質点) で構成されているとします。これを hairNumber 本生成します。節点の数 pointCounthairNumber * hairKnots 個になります。このサイズの頂点バッファオブジェクト (VBO) を準備します。

  //
  // 頂点バッファオブジェクト
  //
  
  // 髪の本数と節点の数
  const GLsizeiptr pointCount(hairNumber * hairKnots);
  
  // 節点の位置の頂点バッファオブジェクトを作成する
  std::array<GLuint, 1> positionBuffer;
  glGenBuffers(positionBuffer.size(), positionBuffer.data());
  
  // 節点の位置の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[0]);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STATIC_DRAW);

頭髪の初期形状

この頂点バッファオブジェクトに節点の位置を格納します。first という配列の各要素は髪の毛の根元の節点の番号、count という配列の各要素は first の各要素の髪の毛の節点の数です。

  // 節点の位置の一つ目の頂点バッファオブジェクトに初期位置を設定する
  std::array<GLint, hairNumber> first;
  std::array<GLsizei, hairNumber> count;
  GLfloat (*const position)[3](static_cast<GLfloat (*)[3]>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY)));
  makeHair(position, hairNumber, hairKnots, first.data(), count.data());
  glUnmapBuffer(GL_ARRAY_BUFFER);

頭髪の初期形状は、半球の表面上にランダムに点を取り、そこから放射状に線分を伸ばして作ります。

  //
  // 頭髪データの作成
  //
  void makeHair(GLfloat (*position)[3], int number, int knots, GLint *first, GLsizei *count)
  {
    for (int i = 0; i < number; ++i)
    {
      // 球面上にランダムに点を生成する
      const GLfloat t(6.2831853f * GLfloat(rand()) / (GLfloat(RAND_MAX) + 1.0f));
      const GLfloat cp(GLfloat(rand()) / GLfloat(RAND_MAX));
      const GLfloat sp(sqrt(1.0f - cp * cp));
      const GLfloat ct(cos(t)), st(sin(t));
      const GLfloat x(sp * ct), y(cp), z(sp * st);
      
      // 一本の髪の毛の最初の節点の番号と節点の数を記録する
      *first++ = knots * i;
      *count++ = knots;
      
      // 球面の法線方向に節点を一直線に配置する
      for (int k = 0; k < knots; ++k)
      {
        const GLfloat o(hairLength * GLfloat(k) / GLfloat(knots - 1) + headRadius);
        (*position)[0] = o * x + headCenter[0];
        (*position)[1] = o * y + headCenter[1];
        (*position)[2] = o * z + headCenter[2];
        ++position;
      }
    }
  }

頂点配列オブジェクトへの組み込み

作成した頂点バッファオブジェクトを頂点配列オブジェクト (VAO) に組み込みます。これをバーテックスシェーダで index が 0 の attribute 変数から取り出せるようにします。

  //
  // 頂点配列オブジェクト
  //
  
  // 頂点配列オブジェクトを作成する
  std::array<GLuint, 1> vao;
  glGenVertexArrays(vao.size(), vao.data());
  
  // 頂点配列オブジェクトの設定を開始する
  glBindVertexArray(vao[0]);
  
  // 節点の位置の頂点バッファオブジェクトを 0 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[0]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);

シェーダプログラム

まず、図形を描画するシェーダプログラムを作成します。

  //
  // プログラムオブジェクト
  //
  
  // 描画用のシェーダプログラムを読み込む
  const GLuint hairShader(ggLoadShader("hair.vert", "hair.frag"));
  const GLint hairMcLoc(glGetUniformLocation(hairShader, "mc"));

バーテックスシェーダのソースプログラムでは、index を 0 番に割り当てた attribute 変数 position の値に、ビュー (視野) 変換行列とプロジェクション (投影) 変換行列の積を格納した uniform 変数 mc をかけたものを、gl_Position に代入するだけです。使ってるマシンのビデオカードでは GLSL 1.5 (OpenGL 3.2) 縛りである必要はないと思うので、1 行目を #version 330 とかにすれば 2 行目の #extension の行は不要です。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
 
// 頂点属性
layout (location = 0) in vec4 position;             // 節点のローカル座標系での位置
 
// uniform 変数
uniform mat4 mc;                                    // ビュープロジェクション変換行列
 
void main()
{
  // スクリーン座標系の座標値
  gl_Position = mc * position;
}

フラグメントシェーダでは index が 0 番の出力変数に色データを代入するだけです。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
 
// フレームバッファに出力するデータ
layout (location = 0) out vec4 fc;                  // フラグメントの色
 
void main()
{
  fc = vec4(0.3, 0.1, 0.0, 1.0);
}

描画処理

この図形の描画は、VAO とシェーダを指定した後、シェーダの uniform 変数 mc の場所にビュー変換行列とプロジェクション変換行列の積を格納して、glMultiDrawArrays()GL_LINE_STRIP を描画します。

  //
  // 描画
  //
  
  // ウィンドウが開いている間くり返し描画する
  while (!window.shouldClose())
  {
    // 画面クリア
    window.clear();
    
    // ビュープロジェクション変換行列
    const GgMatrix mc(window.getMp() * window.getMv());
    
    //
    // 通常の描画
    //
    
    // 頂点配列オブジェクトを選択する
    glBindVertexArray(vao[0]);
    
    // 描画用のシェーダプログラムを使用する
    glUseProgram(hairShader);
    
    // ビュープロジェクション変換行列を設定する
    glUniformMatrix4fv(hairMcLoc, 1, GL_FALSE, mc.get());
    
    // 頂点配列を描画する
    glMultiDrawArrays(GL_LINE_STRIP, first, count, hairNumber);
    
    // フレームバッファを入れ替える
    window.swapBuffers();
  }

glMultiDrawArrays(mode, first, count, number) は glDrawArrays() を次のように呼び出す場合と同等の動作をします。

for (int i = 0; i < number; ++i)
{
  glDrawArrays(mode, first[i], count[i]);
}

節点の位置の更新

本題に入ります。前節までのように GPU の頂点バッファオブジェクト上に置いた頂点属性 (attribute) が変化しないなら、一つの頂点バッファオブジェクトを繰り返し読み出して描画します。

GPU で頂点属性を変更しない場合

しかし、頂点バッファオブジェクトの内容を GPU 側で更新する場合は、入力の頂点属性を保持する頂点バッファオブジェクトの他に、更新後の頂点属性を保持する頂点バッファオブジェクトが別に必要になります。GPU の計算モデルである Stream Processing ではパイプライン上のデータの流れは一方通行であり、計算結果を入力側に戻すようなことは行いません。

GPU で頂点属性を更新する場合

  //
  // 頂点バッファオブジェクト
  //
  
  // 髪の本数と節点の数
  const GLsizeiptr pointCount(hairNumber * hairKnots);
  
  // 節点の位置の頂点バッファオブジェクトを二つ作成する
  std::array<GLuint, 2> positionBuffer;
  glGenBuffers(positionBuffer.size(), positionBuffer.data());
  
  // 節点の位置の一つ目の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[0]);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STREAM_COPY);
  
  // 節点の位置の一つ目の頂点バッファオブジェクトに初期位置を設定する
  std::array<GLint, hairNumber> first;
  std::array<GLsizei, hairNumber> count;
  GLfloat (*const position)[3](static_cast<GLfloat (*)[3]>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY)));
  makeHair(position, hairNumber, hairKnots, first.data(), count.data());
  glUnmapBuffer(GL_ARRAY_BUFFER);

このバッファの内容は GPU 側で頻繁に更新されますので、glBufferData() の引数 usageGL_STREAM_COPY にしています。また二つ目の頂点バッファオブジェクトには初期値を設定する必要がないので、メモリの確保だけを行います。

  // 節点の位置の二つ目の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[1]);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STREAM_COPY);

前にブログに書いていたゴムシミュレータ(2)では、節点 (質点) にかかる力をダイレクトに位置に加算するという手抜きをしていました1。この方法だと中間的な頂点バッファオブジェクトを節約できますが、節点の運動を正しく再現しているとは言えません。

そこで、節点にかかる力から節点の加速度を求め、これをもとに節点の速度を求めて位置を更新します。そのため、節点の現在位置から節点にかかる力を求め、一旦頂点バッファオブジェクトに保存します。これを参照して節点の速度を求め、さらに位置を更新します。この速度は位置と同様に節点の運動の状態として保持しておく必要があるので、これも頂点バッファオブジェクトを二つ用意します。

  // 節点の速度の頂点バッファオブジェクトを二つ作成する
  std::array<GLuint, 2> velocityBuffer;
  glGenBuffers(velocityBuffer.size(), velocityBuffer.data());
  
  // 節点の速度の一つ目の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, velocityBuffer[0]);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STREAM_COPY);

節点の速度の一つ目の頂点バッファオブジェクトの初期値 (初速度) は、とりあえず 0 にしておきます。

  // 節点の速度の一つ目の頂点バッファオブジェクトに初速度を設定する
  GLfloat *const velocity(static_cast<GLfloat *>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY)));
  std::fill(velocity, velocity + pointCount * 3, 0.0f);
  glUnmapBuffer(GL_ARRAY_BUFFER);

節点の速度の二つ目の頂点バッファオブジェクトはメモリの確保だけを行っておきます。

  // 節点の速度の二つ目の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, velocityBuffer[1]);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STREAM_COPY);

最後に節点にかかる力を保持する頂点バッファオブジェクトを作成し、メモリを確保しておきます。これは一つだけで構いません。

  // 節点の力の頂点バッファオブジェクトを作成する
  GLuint forceBuffer;
  glGenBuffers(1, &forceBuffer);
  
  // 節点の力の頂点バッファオブジェクトのメモリを確保する
  glBindBuffer(GL_ARRAY_BUFFER, forceBuffer);
  glBufferData(GL_ARRAY_BUFFER, pointCount * sizeof (GLfloat[3]), 0, GL_STREAM_COPY);

二つの頂点配列オブジェクトへの組み込み

頂点配列オブジェクトも二つ作成します。二つの頂点バッファオブジェクトの切り替えは、頂点配列オブジェクトの切り替えで行います。

  //
  // 頂点配列オブジェクト
  //
  
  // 頂点配列オブジェクトを二つ作成する
  std::array<GLuint, 2> vao;
  glGenVertexArrays(vao.size(), vao.data());

それぞれの頂点配列オブジェクトに頂点バッファオブジェクトを組み込みます。

  // 一つ目の頂点配列オブジェクトの設定を開始する
  glBindVertexArray(vao[0]);
  
  // 節点の位置の一つ目の頂点バッファオブジェクトを 0 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[0]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // 節点の速度の一つ目の頂点バッファオブジェクトを 1 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, velocityBuffer[0]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(1);
  
  // 節点の力の頂点バッファオブジェクトを 2 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, forceBuffer);
  glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(2);

節点にかかる力を保持する頂点バッファオブジェクトは、二つの頂点配列オブジェクトで共有します。

  // 二つ目の頂点配列オブジェクトの設定を開始する
  glBindVertexArray(vao[1]);
  
  // 節点の位置の二つ目の頂点バッファオブジェクトを 0 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer[1]);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // 節点の速度の一つ目の頂点バッファオブジェクトを 1 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, velocityBuffer[1]);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(1);
  
  // 節点の力の頂点バッファオブジェクトを 2 番の attribute 変数で取り出す
  glBindBuffer(GL_ARRAY_BUFFER, forceBuffer);
  glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(2);

テクスチャバッファオブジェクト

節点にかかる力を求めるには、処理対象の節点と隣接する節点のデータを取得する必要があります。しかし、頂点バッファオブジェクトからは処理対象の節点に与えられた頂点属性しか取り出すことができません。そこで、同じ頂点バッファオブジェクトをもとにテクスチャバッファオブジェクトを作成します。これにより頂点バッファオブジェクトをテクスチャとして参照することで、処理対象の節点以外の頂点属性の値を取得します。

  //
  // テクスチャバッファオブジェクト
  //
  
  // 節点の位置のテクスチャオブジェクトを二つ作成する
  std::array<GLuint, 2> positionTexture;
  glGenTextures(positionTexture.size(), positionTexture.data());
  
  // 節点の位置の一つ目のテクスチャオブジェクト
  glBindTexture(GL_TEXTURE_BUFFER, positionTexture[0]);
  glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, positionBuffer[0]);
  
  // 節点の位置の二つ目のテクスチャオブジェクト
  glBindTexture(GL_TEXTURE_BUFFER, positionTexture[1]);
  glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, positionBuffer[1]);

節点にかかる力を求めるシェーダプログラム

節点の位置の更新は、まずその節点にかかる力を求め、それによる加速度を積分して速度を求め、さらに速度を積分して位置を求める、という手順になります。

節点にかかる力を求めるシェーダプログラムには、uniform 変数として、近傍の節点の頂点属性の取得に用いるテクスチャオブジェクトのサンプラ neighbor、一本の髪の毛の根元の節点と先端の節点の頂点番号を設定する endpoint、髪の毛の自然長 l0、頭の中心位置 center、および頭の半径 radius を渡します。

  //
  // プログラムオブジェクト
  //
  
  // 描画用のシェーダプログラムを読み込む
  const GLuint hairShader(ggLoadShader("hair.vert", "hair.frag"));
  const GLint hairMcLoc(glGetUniformLocation(hairShader, "mc"));
  
  // 節点に加わる力の計算用のシェーダプログラムを読み込む
  const std::array<const char *, 1> forceOut = { "force" };
  const GLuint forceShader(ggLoadShader("force.vert", nullptr, nullptr, forceOut.size(), forceOut.data()));
  const GLint forceNeighborLoc(glGetUniformLocation(forceShader, "neighbor"));
  const GLint forceEndpointLoc(glGetUniformLocation(forceShader, "endpoint"));
  const GLint forceL0Loc(glGetUniformLocation(forceShader, "l0"));
  const GLint forceCenterLoc(glGetUniformLocation(forceShader, "center"));
  const GLint forceRadiusLoc(glGetUniformLocation(forceShader, "radius"));

このシェーダプログラムは、次のようになります。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
 
// 定数
const float k0 = 100.0;                             // ばね定数
 
// 頂点属性
layout (location = 0) in vec4 position;             // 節点のローカル座標系での位置
 
// uniform 変数
uniform samplerBuffer neighbor;                     // 近傍の節点を取得するテクスチャバッファオブジェクト
uniform ivec2 endpoint;                             // 一本の髪の毛の最初と最後の節点のインデックス
uniform float l0;                                   // ばねの自然長
uniform vec3 center;                                // 頭の中心位置
uniform float radius;                               // 頭の半径
 
// フィードバックバッファ
out vec3 force;                                     // 節点に加わる力

隣接する節点 (頂点番号 $i$) から受ける力 $f$ は、ばね定数を $k_0$、ばねの自然長を $l_0$ として、Hooke の法則 $f = -k_0 (l - l_0)$ により求めます。$l$ はばねの現在の長さ (length(d)) です。$f$ の向きはばねの伸縮方向 (d / length(d)) とします。

ばねのモデル

// 隣接する節点との間の伸縮に対する抵抗力を求める
vec3 elasticity(const in int i)
{
  // 節点の隣接する節点からの変位
  vec3 d = (position - texelFetch(neighbor, i)).xyz;
  
  // 節点の変位にもとづく力
  return (k0 * l0 / length(d) - k0) * d;
}

頭髪と頭の衝突処理にはペナルティ法を用います。頭にめり込んだ節点には、めり込んだ分だけ頭の外に押し出す力を加えます。頭の外では力が 0 になるように、max() で 0 と比較して大きい方を取っています。ここで $f$ を二乗して 10000 を掛けていることに特に意味はありません2。頭の形をモデリングしたりするのはめんどくさいので球で表しています。頭髪の初期形状自体、半球面上に生やしていましたし。

void main()
{
  // 節点の頭の中心位置からの変位
  vec3 v = position.xyz - center;
  
  // 節点の頭の中心からの距離
  float l = length(v);
  
  // 頭にめり込んだ節点を頭の表面に押し出す力
  float f = max(radius - l, 0.0);
  force = 10000.0 * f * f * v / l;

隣接する節点との距離に応じた力を合計して、その節点にかかる力とします。ただし、根元と先端の節点では、接点のない方からの力を求めないようにします。uniform 変数 endpoint には、毛髪の根元と先端の頂点番号を格納しています。他の力の影響を考慮する場合は、基本的にここに組み込むことになると思います。

  // 節点が根元でなければ隣接する節点から受ける力を加える
  if (gl_VertexID > endpoint.s) force += elasticity(gl_VertexID - 1);
  
  // 節点が先端でなければ隣接する節点から受ける力を加える
  if (gl_VertexID < endpoint.t) force += elasticity(gl_VertexID + 1);
}

gl_VertexID は GLSL の組み込み変数で、処理対象の頂点の番号が格納されています。したがって、positiontexelFetch(neighbor, gl_VertexID) は同じものになります。

節点の位置を更新するシェーダプログラム

節点の位置を更新するシェーダプログラムでは、uniform 変数として、積分計算のタイムステップを設定する dt と、根元の節点の頂点番号を設定する anchor を渡します。

  // 節点の位置の更新用のシェーダプログラムを読み込む
  const std::array<const char *, 2> updateOut = { "newPosition", "newVelocity" };
  const GLuint updateShader(ggLoadShader("update.vert", nullptr, nullptr, updateOut.size(), updateOut.data()));
  const GLint updateDtLoc(glGetUniformLocation(updateShader, "dt"));
  const GLint updateAnchorLoc(glGetUniformLocation(updateShader, "anchor"));

このバーテックスシェーダは、次のようになります。

#version 150
#extension GL_ARB_explicit_attrib_location : enable
 
// 定数
const float m = 0.05;                               // 質点の質量
const vec3 g = vec3(0.0, -9.8, 0.0);                // 重力加速度
const float c = 1.0;                                // 空気抵抗
 
// 頂点属性
layout (location = 0) in vec4 position;             // 節点のローカル座標系での位置
layout (location = 1) in vec3 velocity;             // 節点のローカル座標系での速度
layout (location = 2) in vec3 force;                // 節点が受ける力
 
// uniform 変数
uniform float dt;                                   // タイムステップ
uniform int anchor;                                 // 毛髪の根元の節点の頂点番号
 
// フィードバックバッファ
out vec3 newPosition;                               // 更新された節点のローカル座標系での位置
out vec3 newVelocity;                               // 更新された節点のローカル座標系での速度

先ほど求めて頂点バッファオブジェクトに格納した節点にかかる力を頂点属性 force により取り出して $a = f / m$ より現在の加速度を求めます。ばねのダンパーを考慮するには近傍の速度も必要になってめんどくさいので、ここでは空気抵抗だけ考えることにします。これに重力加速度 $g$ を加えます。この加速度を使ってタイムステップ dt 後の節点の速度 $v$ を求め、これと現在の速度 velocity との平均 0.5 * (velocity + v) により、dt 後の節点の位置を求めます (Heun 法)。

Heun 法

void main()
{
  // 現在の加速度
  vec3 a = (force - c * velocity) / m + g;
  
  // 更新後の速度
  vec3 v = velocity + a * dt;
  
  // 位置と速度の更新 (Heun 法)
  newPosition = position.xyz + step(anchor + 1, gl_VertexID) * 0.5 * (velocity + v) * dt;
  newVelocity = v;
}

なお、根元の節点は頭に張り付いているので、位置を更新しないようにしないといけません。このときシェーダで if 文を使うと、パフォーマンスが大きく悪化することがあるので、組み込み関数 step() を使っています3step(anchor + 1, gl_VertexID) は、根元の節点の番号 anchor + 1 >= gl_VertexID であれば 1、でなければ 0 になります。

頂点位置を更新しながら描画

実際の描画処理の後に、これらのシェーダプログラムを用いた描画処理を追加します。この時はフラグメントシェーダは使用しないので、glEnable(GL_RASTERIZER_DISCARD) によりラスタライザをスキップするようにします。また計算精度を上げるために dt を小さくして繰り返し回数を増すことができるようにしておきます。

  //
  // 描画
  //
  
  // 描画する頂点配列バッファ
  int buffer(0);
  
  // ウィンドウが開いている間くり返し描画する
  while (!window.shouldClose())
  {
    // 画面クリア
    window.clear();
    
    ...
    
    // フレームバッファを入れ替える
    window.swapBuffers();
    
    //
    // 計算
    //
    
    // ラスタライザを止める
    glEnable(GL_RASTERIZER_DISCARD);
    
    // 計算を繰り返す
    for (int l = 0; l < loops; ++l)
    {

節点に加わる力を求めるシェーダを指定して、uniform 変数を設定します。

      //
      // 節点に加わる力の算出
      //
      
      // 節点に加わる力の計算用のシェーダプログラムを使用する
      glUseProgram(forceShader);
      
      // ばねの自然長を設定する
      glUniform1f(forceL0Loc, hairLength / GLfloat(hairKnots - 1));
      
      // 頭の中心位置を設定する
      glUniform3fv(forceCenterLoc, 1, headCenter);
      
      // 頭の半径を設定する
      glUniform1f(forceRadiusLoc, headRadius);

近傍の節点の位置を得るために、テクスチャユニットを指定してテクスチャバッファオブジェクトを結合します。

      // 近傍の頂点の位置のテクスチャバッファオブジェクトのサンプラを指定する
      glUniform1i(forceNeighborLoc, 0);
      
      // 近傍の頂点の位置のテクスチャバッファオブジェクトを結合する
      glActiveTexture(GL_TEXTURE0);
      glBindTexture(GL_TEXTURE_BUFFER, positionTexture[buffer]);

Transform Feedback を有効にして、図形の描画を行います。ここでも glMultiDrawArrays() が使いたいところですが、描画するプリミティブ (GL_LINE_STRIP) ごとに uniform 変数を変える方法がわからなかった4ので、glDrawArrays() を繰り返し呼んでいます。このような値は頂点属性として渡してしまった方がいいかもしれません。

      // 節点の力の頂点バッファオブジェクトを force のターゲットにする
      glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, forceBuffer);
      
      // Transform Feedback を有効にして計算を実行する
      glBeginTransformFeedback(GL_POINTS);
      for (int i = 0; i < hairNumber; ++i)
      {
        glUniform2i(forceEndpointLoc, first[i], first[i] + count[i] - 1);
        glDrawArrays(GL_POINTS, first[i], count[i]);
      }
      glEndTransformFeedback();

次に、節点の位置を更新するシェーダを指定して、uniform 変数を設定します。

      //
      // 節点の位置と速度の更新
      //
      
      // 更新用のシェーダプログラムの使用開始
      glUseProgram(updateShader);
      
      // タイムステップを設定する
      glUniform1f(updateDtLoc, 1.0f / (fps * GLfloat(loops)));

これも先程と同様に Transform Feedback を有効にして、図形の描画を行います。

      // もう一方の節点の位置の頂点バッファオブジェクトを newPosition のターゲットにする
      glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, positionBuffer[1 - buffer]);
      glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, velocityBuffer[1 - buffer]);
      
      // Transform Feedback を有効にして計算を実行する
      glBeginTransformFeedback(GL_POINTS);
      for (int i = 0; i < hairNumber; ++i)
      {
        glUniform1i(updateAnchorLoc, first[i]);
        glDrawArrays(GL_POINTS, first[i], count[i]);
      }
      glEndTransformFeedback();

最後に、二つの頂点配列オブジェクトを入れ替えて、次のステップでの計算に備えます。

      // 頂点配列オブジェクトの選択を入れ替える
      glBindVertexArray(vao[buffer = 1 - buffer]);

繰り返し処理が終了したら、ラスタライザを有効にして、更新した節点の位置による次のフレームの描画に備えます。

    }
    
    // ラスタライザを使用する
    glDisable(GL_RASTERIZER_DISCARD);
  }

今後

曲げ方向の力や陰影付けの追加についても解説する予定ですが、さすがに年末で他にもやらないといけないことがいっぱいあるので、年内に書けるかどうかわかりません。適当に進めておいてください。

  1. これは最急降下法と呼ばれる方法かもしれません。 

  2. 全くないことはないんですけど、アドホックな思いつきでしかありません。 

  3. 節点にかかる力を求めるシェーダでも if 文を使っていますが、この場合は処理を除外しないと 0 による除算が発生するので、この手が使えませんでした。 

  4. uniform buffer とか使えばできるのでしょうか。