JavaScript
WebGL
GLSL
three.js

three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する


概要

この記事では「three.js超入門」と題して、three.jsの基礎からシェーダーの利用までをやっていきます。

ターゲットは主に「canvas表現を触ったことがないフロントエンドエンジニア」を想定しているので、jsの構文などの説明は省略しています。

three.jsのバージョンは執筆時点で最新のr98を使用します。

three.js超入門 第0回 3Dコンピュータグラフィックスの基礎

three.js超入門 第1回 レンダリングまでの流れ

three.js超入門 第2回 アニメーションと時間ベースでの制御

three.js超入門 第3回 マウスやスクロールでのインタラクション

three.js超入門 第4回 DOM要素との連携

three.js超入門 第5回 シェーダー(GLSL)の基礎

three.js超入門 第6回 ShaderMaterialでメッシュを変形、着色する

three.js超入門 第7回 シェーダーに変数を渡す

three.js超入門 第8回 シェーダーをインタラクティブに動かす

three.js超入門 第9回 シェーダーでテクスチャにエフェクトをかける

リポジトリ


three.jsでシェーダーを使うには

マテリアルにShaderMaterialRawShaderMaterialのどちらかを使用します。

ShaderMaterialは、three.js側で便利な変数(モデルの頂点座標やテクスチャ座標など)をシェーダーに自動で挿入してくれます。

逆にRawShaderMaterialは、three.js側での補助が一切ないプレーンな状態のシェーダーを記述したいときに使うクラスです。

シェーダーに慣れるまではShaderMaterialを使用することをおすすめします。


サンプルコード(シェーダーソースを文字列として記述する場合)

GLSLのコードは改行を含むので、JSファイルに直接記述する場合はテンプレート文字列を使うと書きやすいです。


ShaderMaterialSample.js

import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';

// 頂点シェーダーのソース
const vertexSource = `
void main() {
gl_Position = vec4(position, 1.0);
}
`
;

// ピクセルシェーダーのソース
const fragmentSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
;

// シェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
vertexShader: vertexSource,
fragmentShader: fragmentSource
});



サンプルコード(シェーダーソースを別ファイルにする場合)

テンプレート文字列ではエディタのシンタックスハイライトが効かないのと、コードごとにファイルを分けたいので、webpack-glsl-loaderを使ってシェーダーの拡張子(.vert .frag .glsl)を読めるようにします。


webpack.config.js

// 省略

module.exports = {
// 省略
module: {
rules: [
{
test: /\.(vert|frag|glsl)$/,
use: {
loader: 'webpack-glsl-loader'
}
}
]
}
};


shader.vert

void main() {

gl_Position = vec4(position, 1.0);
}


shader.frag

void main() {

gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}


ShaderMaterialSample.js

import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';

// シェーダーソース
import vertexSource from './shader.vert';
import fragmentSource from './shader.frag';

// シェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
vertexShader: vertexSource,
fragmentShader: fragmentSource
});



実際のコード

リポジトリのプロジェクトルートでnpm startしてローカルサーバーを起動し、http://localhost:3000/00_shader_empty/にアクセスします。

10 x 10の平面メッシュがワイヤーフレームで表示されます。

スクリーンショット 2019-03-19 17.20.28.png

プロジェクトフォルダのsrc/00_shader_empty/にシェーダー用のテンプレートを用意しているので、これを書き換えていきましょう。

00_shader_empty

├── Canvas
│   ├── index.js
│   └── shaders
│   ├── shader.frag
│   └── shader.vert
└── index.js

まず、Canvasクラスの中身はこうなっています。


Canvas/index.js

import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';

import { OrthographicCamera } from 'three/src/cameras/OrthographicCamera';
import { Scene } from 'three/src/scenes/Scene';
import { PlaneGeometry } from 'three/src/geometries/PlaneGeometry';
import { ShaderMaterial } from 'three/src/materials/ShaderMaterial';
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial';
import { Mesh } from 'three/src/objects/Mesh';
import { Vector2 } from 'three/src/math/Vector2';

// シェーダーソース
import vertexSource from './shaders/shader.vert';
import fragmentSource from './shaders/shader.frag';

export default class Canvas {
constructor() {
// ウィンドウサイズ
this.w = window.innerWidth;
this.h = window.innerHeight;

// レンダラーを作成
this.renderer = new WebGLRenderer();
this.renderer.setSize(this.w, this.h);// 描画サイズ
this.renderer.setPixelRatio(window.devicePixelRatio);// ピクセル比

// #canvas-containerにレンダラーのcanvasを追加
const container = document.getElementById("canvas-container");
container.appendChild(this.renderer.domElement);

// カメラを作成(背景シェーダーだけならパースいらないので、OrthographicCameraをつかう)
this.camera = new OrthographicCamera(-1, 1, 1, -1, 0, -1);

// シーンを作成
this.scene = new Scene();

// 平面をつくる(幅, 高さ, 横分割数, 縦分割数)
const geo = new PlaneGeometry(2, 2, 10, 10);

// シェーダーソースを渡してマテリアルを作成
const mat = new ShaderMaterial({
vertexShader: vertexSource,
fragmentShader: fragmentSource,
wireframe: true
});

this.mesh = new Mesh(geo, mat);

// メッシュをシーンに追加
this.scene.add(this.mesh);

// 描画ループ開始
this.render();
}

render() {
// 次のフレームを要求
requestAnimationFrame(() => { this.render(); });

// ミリ秒から秒に変換
const sec = performance.now() / 1000;

// 画面に表示
this.renderer.render(this.scene, this.camera);
}
};


今回はページの背景をシェーダー表現にしたいだけなので、ジオメトリには平面のPlaneGeometryを使用し、カメラはPerspectiveCameraではなくOrthographicCamera(パースのないカメラ)を使用しています。

ファイル上部でimportしたシェーダーソース(shader.vert shader.frag)をShaderMaterialに渡してシェーダーマテリアルを作成しています。


頂点シェーダー

次に頂点シェーダー(shader.vert)を見てみましょう。


shader.vert

// vertex shader ( 頂点シェーダー )

// このファイルに各頂点ごとの処理を記述します

void main() {
vec3 pos = position;// position: ShaderMaterialで補完される vec3 型(xyz)の変数。ジオメトリの頂点のこと。

gl_Position = vec4( pos, 1.0 );
}


positionは、ShaderMaterial側で自動的に宣言してくれているvec3型の変数で、ジオメトリの頂点座標を表しています。

gl_Positionvec4型の座標を代入することで頂点の位置を決定するGLSLの組み込み変数です。


頂点シェーダーでメッシュを変形させてみる


shader.vert

void main() {

vec3 pos = position;

pos.y = ( pos.y * 0.5 ) + sin( pos.x * 3.0 ) * 0.5;// 縦を半分のサイズにして、sinでy座標を歪ませる

gl_Position = vec4( pos, 1.0 );
}


スクリーンショット 2019-03-19 17.34.28.png

たった1行追加するだけでメッシュを変形させることができました。

このように、CPUだといちいちforループを回さないとできないことが、シェーダーを使うとほんの数行で実現できます。すばらしいですね。

※ あとに影響がでるので頂点シェーダーの変更は元に戻しておきましょう。


shader.vert

void main() {

vec3 pos = position;

gl_Position = vec4( pos, 1.0 );
}



ピクセルシェーダー

次にピクセルシェーダーをいじるので、PlaneGeometryの分割を1 x 1にして、ワイヤーフレーム表示をオフにしておきます。


Canvas/index.js

// 平面をつくる(幅, 高さ, 横分割数, 縦分割数)

const geo = new PlaneGeometry(2, 2, 1, 1);

// シェーダーマテリアルに GLSL のソースを渡す
const mat = new ShaderMaterial({
vertexShader: vertexSource,
fragmentShader: fragmentSource,
wireframe: false// ワイヤーフレームをオフ
});


ピクセルシェーダー(shader.frag)を見てみましょう


shader.frag

// fragment shader ( フラグメントシェーダー、ピクセルシェーダー )

// このファイルに各ピクセルごとの処理を記述します

void main() {
vec4 color = vec4(1.0, 1.0, 1.0, 1.0);// rgba

gl_FragColor = color;// gl_FragColor に vec4 型(rgba)の色を入れることでピクセル色を決定する。
}


gl_FragColorvec4型の色を代入することでピクセルの色を決定するGLSLの組み込み変数です。

vec4(1.0, 1.0, 1.0, 1.0)で、rgbaが全て1.0となっているので、白色になっています。


ピクセルシェーダーでメッシュの色を変えてみる


shader.frag

void main() {

vec4 color = vec4(1.0, 0.0, 0.0, 1.0);// 赤

gl_FragColor = color;
}


スクリーンショット 2019-03-19 17.54.43.png

メッシュにピクセルシェーダーで色をつけられました。

シェーダーの最低限の機能は以上です。

次回はさらに踏み込んで、頂点シェーダーからピクセルシェーダーに変数を渡したり、CPUからシェーダーに変数を渡す方法をやります。