🍕

three.jsでディゾルブエフェクトを実装してみる

2022/11/15に公開

ディゾルブ(Dissolve:溶解する)エフェクトとはその名の通り、溶けるように消えたり現れたりする3DCG表現のことです。(多分)

threejs-my-dissolve-sample1.gif

こちらでパラメータを操作して遊ぶことができます。
他にも紙が燃えて消える演出なんかにも使えそうです。

自作ゲームで使いたいと思い調べたんですが、意外とthree.jsでの方法が明示されてる記事などが見つからず、自力実装するのになかなか手間取ったので方法をまとめてみました。

簡易的な方法と、カスタムシェーダーを利用した二つの方法を紹介します。

前準備:ノイズマップの用意

実装にあたっては以下のような白黒のノイズマップ画像が必要になります。

noise.png

こちらがエフェクトの模様のソースとなります。
(こちらの画像は好きに使ってもらって大丈夫です)

以降コードではdissolveMapという変数で以下のようにロード・展開します。

const dissolveMap = new TextureLoader().load("./assets/my-noise.png");

three.js デフォルト機能のalphaMap(とalphaTest)プロパティを利用して実現する

alphaMap機能を用いることで簡易的にそれっぽいエフェクトを実現することができます。

const material = new MeshStandardMaterial({transparent: true});
material.alphaMap = dissolveMap;
material.alphaTest = 0.5;

こちらの方法は公式のサンプルで即座に試すことができます。
以下の手順でパラメータを操作してみてください。

設定方法の図

参考アニメgif

MeshBasicMaterialクラスから利用できます。

カスタムシェーダーで実現する

上記の演出では物足りない、自身でもっと拡張したい場合はシェーダーをいじる必要があります。

以下拡張例を紹介しますが、原理の説明に際して以下の動画に一度目を通しておくと内容が理解しやすくなるかもしれません。
(こちらUnityでの実装方法かつ英語ですが、途中の図の説明だけでも何となく理解できるかと思います)

https://www.youtube.com/watch?v=taMp1g1pBeE

さてカスタムシェーダーといえばShaderMaterialですが、汎用性の高いMeshStandardMaterialを拡張する形のほうが実用性がありそうなのでそのようにします。

// カスタムクラス
class DissolvableMeshStandardMaterial extends MeshStandardMaterial {
  constructor(...params) {
    super(...params);

    Object.assign(this.userData, {
      uniforms: {
        dissolveMap: { value: null },
        uThreshold: { value: 1.0 },
        uEdgeWidth: { value: 0.01 },
        uEdgeColor: { value: [0, 0.89, 1.0] },
      },
    });
  }

  /**
   * @override
   */
  onBeforeCompile(shader: Shader, _renderer: WebGLRenderer) {
    // uniform同期
    Object.assign(shader.uniforms, this.userData.uniforms);

    // vertexShader拡張
    shader.vertexShader = shader.vertexShader.replace(
      "void main() {",
      /* glsl */ `
      varying vec2 vDisUv;
      void main() {
        vDisUv = vec2( uv.x, uv.y );
      `
    );

    // fragmentShader拡張その1:uniform宣言
    shader.fragmentShader = shader.fragmentShader.replace(
      "void main() {",
      /* glsl */ `
      uniform sampler2D dissolveMap;
      uniform float uThreshold;
      uniform float uEdgeWidth;
      uniform vec3 uEdgeColor;
      varying vec2 vDisUv;
      void main() {
      `
    );

    // fragmentShader拡張その2:diffuseColor計算処理追加
    shader.fragmentShader = shader.fragmentShader.replace(
      "vec4 diffuseColor = vec4( diffuse, opacity );",
      /* glsl */ `
      float alpha = opacity;
      float noize = texture2D(dissolveMap, vDisUv).g;

      if ( noize > uThreshold) {
        alpha = 0.0;
      }

      vec3 color = diffuse;
      if ( noize + uEdgeWidth > uThreshold ) {
        color = uEdgeColor;
      }

      vec4 diffuseColor = vec4( color, alpha );
      `
    );
  }
}
  • onBeforeCompileを介して元々のMaterialのシェーダー書き換えやuniform拡張を行っています。詳細はこちらの記事に譲ります。
  • vertexシェーダーのほうはvDisUvという独自のvaring変数を転送しています。
    • 元々デフォルトでvUvという変数が存在していてそちらを利用したいところですが、こちらは何らかのmap系プロパティが有効でないと宣言・定義されないという問題があり、必ずuvを受け取りたいということで独自に定義します。(この辺りは要改善ポイント)
  • fragmentシェーダーの方では拡張uniformの宣言、およびdiffuseColorという元々ある変数の計算方法を書き換えています。
    • dissolveMapテクセルのgパラメータ(noize値)が一定値(threshold)以下かどうかで、描画の可否(alpha値)を設定
    • noizeにuEdgeWidthを加算超過した部分には指定エッジ色を付けます

使用例

// 適当なGeometry用意
const geo = new SphereBufferGeometry();

// Materialをセットアップ
const mat = new DissolvableMeshStandardMaterial({
  transparent: true, // transparentプロパティは基本的にtrueにしておかないと反映されない
  side: DoubleSide, // 消えている最中、裏側も描画したい
});
// パラメータを設定
mat.userData.uniforms.dissolveMap.value = dissolveMap;
mat.userData.uniforms.uThreshold.value = 0.5;

// メッシュ作成・追加
const mesh = new Mesh(geo, mat);
scene.add(mesh);

また冒頭の🐵(スザンヌ)モデルを使ったサンプルのコードは以下で見られます(こちらではaccessorなどを利用してより使い勝手を良くしています)
https://github.com/pentamania/threejs-dissolve-effect-sample

その他補足など

  • transparentの設定は忘れがちなので、コンストラクタでデフォルトtrue状態にしてもいいかも
  • デフォルト設定では消えている最中のメッシュ裏側は描画されませんので、適宜sideプロパティをDoubleSideに設定します
  • 複雑なGeometryに対しては綺麗に動作しない可能性があります
  • やろうと思えば、dissolveMapデータ自体をシェーダー側で動的に作成することも可能です。その場合シード値などによって模様を変えることができるようになったりします(実装方法はこちらのページだったり、「glsl シェーダー ノイズ」などでググったりしてみてください)

Discussion