LoginSignup
7
6

More than 3 years have passed since last update.

three.jsのRawShaderMaterialを使い回す

Posted at

three.jsのRawShaderMaterial(またはShaderMaterial)を使うとシェーダーを自分で書くことができます。このときにシェーダーのコードは同じだけどuniform変数の値だけを変えて複数のメッシュを描画したいときがあります。

単純な例ですが、以下のように色だけを変えて複数の四角形をRawShaderMaterialで描画したいとします。
ReuseRawShaderMaterial_Small.png

何も考えずに書くと、次のコードのようにシェーダー自体は同じなのにも関わらず一つのメッシュごとにマテリアルを作成してしまいます。しかし、これではCPU的にもメモリ的にも無駄な作業をしているように見えます(正確なパフォーマンスの検証はしていませんが...)。

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const plane = new THREE.PlaneBufferGeometry(0.75, 0.75);

const vertexShader = 
`uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

attribute vec3 position;

void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`;
const fragmentShader =
`precision highp float;

uniform vec3 color;

void main() {
  gl_FragColor = vec4(color, 1.0);
}
`;

const planeNum = new THREE.Vector2(10, 10);
for (let x = 0; x < planeNum.x; x++) {
  for (let y = 0; y < planeNum.y; y++) {
    const material = new THREE.RawShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      uniforms: {
        color: {
          value: new THREE.Color(x / planeNum.x, y / planeNum.y, 1.0)
        },
      }
    });
    const mesh = new THREE.Mesh(plane, material);
    mesh.position.set(x - 0.5 * planeNum.x, y - 0.5 * planeNum.y, 0);
    scene.add(mesh);
  }
}

camera.position.z = 15.0;
renderer.render(scene, camera);

Object3D.onBeforeRenderを使う方法

ShaderMaterialの解説に書いてあるようにObject3D.onBeforeRender内でuniform値を更新することで、1つのマテリアルを使いまわして複数のメッシュを描画することができます。更新するためにはマテリアルのuniformsNeedUpdatetrueにすることも必要です。

You're recommended to update custom Uniform values depending on object and camera in Object3D.onBeforeRender because Material can be shared among meshes, matrixWorld of Scene and Camera are updated in WebGLRenderer.render, and some effects render a scene with their own private cameras.
(筆者訳) マテリアルがメッシュ間で共有されたり、シーンやカメラのmatrixWorldがWebGLRenderer.renderで更新されたり、自分自身のプライベートなカメラでシーンを描画するエフェクトがあるため、オブジェクトまたはカメラのObject3D.onBeforeRendern内でカスタムのuniform値を更新することをおすすめします。

ということで、Object3D.onBeforeRenderを使用して上述したコードを書き直すと以下のようになります。RawShaderMaterialは最初に1回だけ作成して使い回されていることがわかると思います。

...
const material = new THREE.RawShaderMaterial({
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  uniforms: {
    color: {
      value: null,
    },
  },
});

const planeNum = new THREE.Vector2(10, 10);
for (let x = 0; x < planeNum.x; x++) {
  for (let y = 0; y < planeNum.y; y++) {
    const mesh = new THREE.Mesh(plane, material);
    mesh.position.set(x - 0.5 * planeNum.x, y - 0.5 * planeNum.y, 0);
    mesh.onBeforeRender = () => {
      mesh.material.uniforms.color.value = new THREE.Color(x / planeNum.x, y / planeNum.y, 1.0);
      mesh.material.uniformsNeedUpdate = true;
    };
    scene.add(mesh);
  }
}
...

ShaderMaterial.clone を使う方法

別の方法として、ShaderMaterial.cloneを使う方法があります。これは正確に言うとメッシュごとにマテリアルを作成することになるのですが、解説に書いてあるようにコンパイル済みのシェーダーが共有されるので効率化に繋がります。

Generates a shallow copy of this material. Note that the vertexShader and fragmentShader are copied by reference, as are the definitions of the attributes; this means that clones of the material will share the same compiled WebGLProgram. However, the uniforms are copied by value, which allows you to have different sets of uniforms for different copies of the material.
(筆者訳) このマテリアルの浅いコピーを生成します。頂点シェーダーとフラグメントシェーダーは参照としてコピーされます。つまりマテリアルのクローンは同じコンパイル済みのWebGLProgramを共有します。しかしuniformsは値としてコピーされるので、異なるマテリアルのコピーには異なるuniformの組み合わせを持たせることができます。

ShaderMaterial.cloneを使用すると次のようなコードになります。

...
const planeNum = new THREE.Vector2(10, 10);
for (let x = 0; x < planeNum.x; x++) {
  for (let y = 0; y < planeNum.y; y++) {
    const clonedMaterial = material.clone();
    clonedMaterial.uniforms.color.value = new THREE.Color(x / planeNum.x, y / planeNum.y, 1.0);
    const mesh = new THREE.Mesh(plane, clonedMaterial);
    mesh.position.set(x - 0.5 * planeNum.x, y - 0.5 * planeNum.y, 0);
    scene.add(mesh);
  }
}
...
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6