びわの家ブログ

three.jsのデフォルトのマテリアルを拡張する

結論にたどり着くまでに半日くらいかかったのでメモ。
three.jsでなにかをつくる場合、ShaderMaterialもしくはRawShaderMaterialを使って全部自前で書いてもいいけどライブラリで用意されているマテリアルをちょこっとだけ改造すれば事足りることが多いと思う。というわけで自分がたどり着いた結論をご紹介。

結論

この公式サンプルを見ればOK
https://threejs.org/examples/#webgl_materials_modified

色々調べたが灯台もと暗しで公式にサンプルがあったという結論。
一応一通りみたつもりだったけど多すぎてどれが目的のサンプルか判断できないあるある。。

Exampleを見ろ!ではあまりにも内容がないのでかんたんに自分が書いたコードを紹介して解説しておく。

コード例

import * as THREE from 'three'

class CustomPhongMaterial extends THREE.MeshPhongMaterial{
  constructor(param){
    super(param);
    this.onBeforeCompile = this.overwriteShader;
  }
  overwriteShader(shader){
    shader.uniforms.customUniform = {type: 'f', value: 1};
    
    // 追加したいコード行    
    const insertUniform = `
uniform float customUniformValue;
`
    // シェーダー内部のどこに追加するかは文字列で指定
    shader.fragmentShader = shader.fragmentShader.replace('#define PHONG', insertUniform);

    const insertMain = `
outgoingLight.r = customUniformValue;
`
    shader.fragmentShader = shader.fragmentShader.replace('#include <envmap_fragment>', insertMain);
    
    // userDataに格納
    this.userData.shader = shader;
  }
}

export default CustomPhongMaterial

  • THREE.MeshPhongMaterialを継承したクラスをつくり、そこにexampleのように拡張を書いていく
    • そうすることによってnew CustomPhongMaterial(param)とかで初期化ができる
  • onCompileBeforeにclass内部のイベント関数を指定する
  • overwriteShader関数で拡張を書く
    • shader.uniform.hogeでhogeというuniform変数をjs側に作っておく
    • GLSLの拡張はshader.fragmentShader.replace関数で第一引数に任意のコードを挿入したい行のコード、第二引数に自分のコードを指定する
    • 今回は上の方にuniform変数の定義とmain関数内にその変数を使ったかんたんなコード(R値を変えるだけ)を書いてみた
    • userDataにshaderオブジェクトを入れておく(これは動的にuniform値を変更するときに使う)
  • ちなみにデフォルトのシェーダーのコードはgithubで確認するとかんたん

uniformの更新はサンプルを見ながら適当にやればいい。(以下の部分)
注意点として、レンダリングの最初のフレームでonCompileBeforeが呼ばれるのでuserData.shaderのnullチェックをしないとエラーになる。最初のuniform値を指定したい場合はmaterialのコンストラクタなどで渡すなどしなくてはならない。

const shader = child.material.userData.shader;

if ( shader ) {
  shader.uniforms.time.value = performance.now() / 1000;
}

補足: customProgramCacheKeyについて
customProgramCacheKey関数内ではシェーダーにバリエーションが有る場合に別のシェーダーとしてコンパイルするかという条件を書いておく場所。
文章で説明が難しいがthree.jsのサンプルの使われ方だとonBeforeCompile内でシェーダーコードの書き換えをする際にamountが定数で埋め込まれている。その場合amountの値が違うだけの複数のシェーダーバリエーションが必要なのでcustomProgramCacheKeyの中でバリエーションを作るかどうかの判定を行う。自分が書いた方のコードは特にそういったバリエーションを必要としないのでcustomProgramCacheKeyは使わなかった。

ドキュメントを読んだだけの知識なので本当かどうかは確認していないが。

別の方法も一応見つけたのでメモ

別の方法でやっている例をみつけてた最初に試してみたが、うごいたもののおすすめはしない。
ただこのサンプルの絵結構きれい。

https://jsfiddle.net/gsynuh/bc1q9prL

  • このコードのままでは現在のthree.jsのバージョンとfsのコードが違っていて黒くなる
  • ShaderMaterialとして作成されるので.mapなどのデフォルトのユニフォームもmaterial.uniforms.map.value=mapと書く必要がある
  • そしてmaterial.mapがnullだとコンパイル時にmap関連の変数定義部分のコードが省かれる
  • など色々と面倒なことが発生する(した)