本サイトは記事内に広告を含む場合があります

JavaScript

【Three.js入門】絶対に知っておくべき「Shader Material」でオリジナルのシェーダーを実装する方法

【Three.js入門】絶対に知っておくべき「Shader Material」でオリジナルのシェーダーを実装する方法

皆さんは「Three.js」と言うJavaScriptライブラリをご存知でしょうか?

Three.jsはJavaScriptで3Dグラフィックを描画できるようにしてくれるライブラリでして、難しい数学の知識や3D分野にあまり精通していない人でも直感的にプログラムを書くことができるという特徴があります。

しかし、実際に仕事で扱えるようになるためにはThree.jsに元々用意されている機能に頼るだけでは実現が困難な場合が多いのが事実なのです。

Three.jsは単純な形状のオブジェクトや、サンプルに用意されているマテリアルであれば容易に実装することが可能ですが、やはり難しい箇所がラッピングされていてブラックボックス化しているので、なかなかそこから発展しにくいということがよくあります。

そこで、今回はThree.jsの簡単な入門方法と「Shader Material」を使ったオリジナルのシェーダーを実装する方法について詳しく解説していきます。

また、Three.jsのダウンロード方法や、細かい使い方についての解説は下記ページをご覧ください。

JavaScript製ライブラリ『Three.js』の一番わかりやすい使い方
JavaScript製ライブラリ『Three.js』の一番わかりやすい使い方【WebGLを知らなくても理解できます】

フロントエンド開発をしていてcanvasでリッチな描画を作る勉強をしていると、『Three.js』という言葉にたどり着くことが多いのではないでしょうか? 『Three.js』はWeGLという主に3D描 ...

続きを見る

 

 

【入門編】Three.jsでボックスを描画する方法

【入門編】Three.jsでボックスを描画する方法

まずは、Three.jsでもっとも簡単なボックスの形状をしたオブジェクトを描画する方法について解説します。

ボックス形状を描画するためにはThree.jsに用意されている「BoxGeometry」メソッドを使います。

その時のオプションに任意の色を「color」プロパティに与えることで、オブジェクトの色を決定できます。

レンダーラーやカメラなどの解説については、冒頭で紹介したページを見ていただければ確認できますので、ここでの詳しい説明については省略します。

「BoxGeometry」を使った実際のJavaScriptサンプルコードは下記になります。

//キャンバスのサイズ
let canvas = {
    width: window.innerWidth,
    height: window.innerHeight
};

//カメラ
let camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 1, 1000);
camera.position.z = 125;

//シーン、レンダラー
let scene = new THREE.Scene();
let renderer = new THREE.WebGLRenderer();
renderer.setSize(canvas.width, canvas.height);

//ジオメトリ、マテリアル、メッシュ
document.body.append(renderer.domElement);
let geometry = new THREE.BoxGeometry(50, 50, 50);
let material = new THREE.MeshBasicMaterial({
    color: 0xffffff, //白色
    //wireframe: true
});
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

//レンダリングループ
function rendering() {
    window.requestAnimationFrame(rendering);

    renderer.render(scene, camera);

    //アニメーション
    mesh.rotation.x += 0.01;
    mesh.rotation.y += 0.01;
}
rendering();

//リサイズ対応
window.addEventListener('resize', function() {
    //キャンバスの変数を更新
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    camera.aspect = canvas.width / canvas.height;

    renderer.setSize(canvas.width , canvas.height);
});

 

【ShaderMaterial】Three.jsで自作したシェーダーを実装する方法

【ShaderMaterial】Three.jsで自作したシェーダーを実装する方法

Three.js入門したての頃は単純な3D表現をするだけで満足してしまいますが、それ以上のことをしようとした場合は、多少Webのコーディングからは離れますが、別のことも学ばなければなりません。

そこで今回紹介するのが「GLSL (OpenGL Shading Language)」と呼ばれるシェーディング言語です。

シェーディングとは3Dの物体に陰影をつけたりして立体感を出すための技法のことを言い、それを実現するためにはシェーダーと呼ばれるものを書く必要があります。

ここで紹介するシェーダーの種類は「Vertex Shader」「Fragment Shader」の2種類でして、それらを使うことで陰影をつける以外にも、ジオメトリの形状を変形させたり、表面に動的な模様を描いたりすることができます。

VertexShaderはジオメトリの頂点情報を司り、Fragment Shaderはジオメトリの表面の色情報を司ります。

Three.jsでGLSLを扱うためにはにデフォルトで用意されている「ShaderMaterial」メソッドを使うことでオリジナルのシェーダーを実装することができます。

詳しい使い方については、下記で解説しておりますので、ご覧ください。

 

uniforms変数の定義

まず、ShaderMaterialの解説に入る前に「uniforms変数」と言うものを定義しておく必要があります。

このuniform変数は、後々のアニメーションの為に使う時間経過の情報であったり、canvasの解像度などの情報を保持しておくために使用します。

ここでは、uniforms.timeと言う形で経過時間の値とuniforms.resolutionと言う形で解像度の情報を取り出せるように変数を定義してみます。

また、uniforms変数を定義する際には「type」プロパティに「型」の宣言をする必要があります。

それ以外にも、GLSLでは基本的に整数を受け付けないため「浮動小数点数」である「float」型を使用する必要があります。

//シェーダーに送信する値
let uniforms = {
    time: { //時間
        type: 'f', //浮動小数点
        value: 0.0
    },
    resolution: { //canvasの解像度
        type: 'v2', //2次元ベクトル
        value: new THREE.Vector2()
    }
};

 

ShaderMaterialメソッドの使い方

次にShaderMaterialメソッドの使い方ですが、「uniforms」「vertexShader」「fragmentShader」プロパティに値をセットすることで自作のシェーダーを実行することができます。

以下の例では、先ほど作成したuniforms変数を与えて、HTMLファイルに記述したシェーダーの記述内容「innerHTML」で取得しています。

合わせて、レンダリングループと画面のリサイズ中にもuniformsの情報を更新してシェーダーに新しい値を送っておく必要があります。

また、HTMLにシェーダーを書く際はscriptのtype属性をJavaScriptとして認識させないために「shader」と記述しましょう。

 

HTML

<script type="shader" id="vertex-shader">
void main() {
}
</script>

<script type="shader" id="fragment-shader">
void main() {
}
</script>

 

JavaScript

let canvas = {
  width: window.innerWidth,
   height: window.innerHeight
};

let scene = new THREE.Scene();
let renderer = new THREE.WebGLRenderer();

let camera = new THREE.Camera();
camera.position.z = 1;

let geometry = new THREE.PlaneBufferGeometry(2, 2);

let material = new THREE.ShaderMaterial({
    uniforms: uniforms,
    vertexShader: document.querySelector('#vertex-shader').innerHTML,
    fragmentShader: document.querySelector('#fragment-shader').innerHTML,
    //wireframe: true
});

let mesh = new THREE.Mesh(geometry, material);

scene.add(mesh);
//レンダリングループ
function rendering() {
    window.requestAnimationFrame(rendering);

    renderer.render(scene, camera);
        
    uniforms.time.value += 0.025;
}
rendering();

//リサイズ
window.addEventListener('resize', function() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    uniforms.resolution.value.x = canvas.width;
    uniforms.resolution.value.y = canvas.height;
});

 

【gl_FragColor】GLSLで色を表示する方法

【gl_FragColor】GLSLで色を表示する方法

まずはGLSLのHello Worldとして画面に色を表示するだけのシンプルなものから実装してみましょう。

書き方は非常にシンプルで、初めてコードを見る人でもなんとなく理解できるかと思います。

また、今回の解説ではFragment Shaderを書き換えることがほとんどですので、Vertex Shaderに関してはコピペしてください。

そして、下記が色を表示するためのサンプルコードなのですが、「gl_FragColor」と言う変数に4次元のベクトルを代入することによって画面に赤色を表示しています。

引数にはCSSでよく見るパターンのように左から順に「rgba」となっており、数値はfloat型にする必要があります。

サンプルの実行結果はコチラ

 

Vertex Shader

<script type="shader" id="vertex-shader">
void main() {
    gl_Position = vec4(position, 1.0);
}
</script>

 

Fragment Shader

<script type="shader" id="fragment-shader">
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
</script>

 

【gl_FragCoord】GLSLの座標の表し方

【gl_FragCoord】GLSLの座標の表し方

GLSLの描画範囲はそのまま扱うと、広すぎるため大きさを正規化してあげる必要がります。

「gl_FragCoord」と言う変数が描画領域の全てのピクセルを表しているのでが、以下の例では「(gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y)」という風にすることによって描画領域の中心を「0」として端から端までを「-1 ~ 1」の範囲に正規化しています。

そして、座標と描画する色の情報を合わせることで右に行くほど黒から赤色になるグラデーションを表示することができます。

また、先ほどuniforms変数に定義した「resolution」の情報は「uniform vec2 resolution;」という形でシェーダー側で取得できます。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform vec2 resolution;

void main() {
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    gl_FragColor = vec4(position.x, 0.0, 0.0, 1.0);
}
</script>

 

【length関数】ベクトルの長さを測る

【length関数】ベクトルの長さを測る

次にGLSLをもっと使いこなすために便利なビルトイン関数である「length」の説明をします。

このlength関数を使いこなすことによって、より高度なレイマーチングなどの表現も可能になってくるので、使い方を覚えておくことをオススメします。

length関数はベクトルの長さを返す関数なのですが、原理は三平方の定理 (ピタゴラスの定理) と一緒で「sqrt(pow(x, 2) + pow(y, 2))」の結果と同じ値を返します。

以下の例では引数に正規化した座標情報を渡すことによって赤く輝くオーブのようなもの描画しています。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform vec2 resolution;

void main() {
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    float color = 0.1 / length(position);
    gl_FragColor = vec4(color, 0.0, 0.0, 1.0);
}
</script>

 

GLSLでアニメーションさせる方法

GLSLでアニメーションさせる方法

GLSLを使ってアニメーションをするためには、先ほどJavaScript側で用意した「uniforms.time.value」の値を使いますので、シェーダーのプログラムに「uniform float time;」と記述する必要がります。

そして、取得した時間経過の情報をsin関数の中に入れて「-1 ~ 1」の範囲にして、abs関数で絶対値を取ることによって「0 ~ 1」の形になるようにしています。

また、先ほどはオーブのようなグラデーションを表示していましたがfloor関数で切り捨てを行い、ベタ塗りの円を描画しています。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform float time;
uniform vec2 resolution;

void main() {
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    float radius = abs(sin(time));
    float color = floor(radius / length(position));
    gl_FragColor = vec4(color, 0.0, 0.0, 1.0);
}
</script>

 

【応用編】円を複数描画する方法

【応用編】円を複数描画する方法

先ほどの解説で円が描画できたので、次は複数の円を同時に描画してみたいと思います。

ポイントはcirlce関数と「sin(position.x * 10.0);」としている所でして、circle関数にはpositionと円の半径の値を与えて内側の色と外側の色をどの座標に色を塗るかを決定し、sin関数で座標を細かく区切ることによって円を複数描画しています。

また、cirlce関数は真偽値を返すようになっており、返り値がtrueだった場合は「circleColor」の値がdistColorに代入され、falseだった場合は「backgroundColor」の値が代入されるようになっています。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform float time;
uniform vec2 resolution;

bool circle(vec2 position, float radius) {
    if(length(position) <= radius) {
        return true;
    } else {
        return false;
    }
}

void main(){
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    position.x -= time / 1.5;
    position.y += time / 1.5;
    position.x = sin(position.x * 10.0);
    position.y = sin(position.y * 10.0);
    vec4 distColor;
    vec4 backgroundColor = vec4(1.0, 0.0, 0.25, 1.0);
    float radius = 0.5;
    vec4 circleColor = vec4(0.0, 0.0, 0.5, 1.0);
    if(circle(position, radius)) {
        distColor = circleColor;
    } else {
        distColor = backgroundColor;
    }
    gl_FragColor = distColor;
}
</script>

 

【応用編】四角形を複数描画する方法

【応用編】四角形を複数描画する方法

円ばかり描いていては進歩しませんので、次はタイル状に四角形を敷き詰めたものを作っていきます。

ポイントは新しく登場する「step関数」でして、第一引数には基準となる点を指定して、第二引数には座標を受け取っています。

「step関数」は引数の値に応じて「0.0または1.0」を返す関数でして、サンプルのような四角形を複数描画する表示結果を得ることができています。

また、条件分岐の「color」の値を変更することで、任意の色合いに変更することも可能です。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform float time;
uniform vec2 resolution;

void main(){
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    position.y -= time / 1.5;
    vec4 distColor;
    float background = step(0.0, sin(position.x * 12.5) * sin(position.y * 12.5));
    if(background == 1.0) {
        vec4 color = vec4(vec3(1.0), 1.0);
        distColor = color;
    } else if(background == 0.0) {
        vec4 color = vec4(0.0, 0.0, 0.0, 1.0);
        distColor = color;
    }
    gl_FragColor = distColor;
}
</script>

 

【応用編】円と四角形を両方描画する方法

【応用編】円と四角形を両方描画する方法

円と四角形について分けて考えてきましたが、ここでは同じ描画領域に表示させてみたいと思います。

そのためには、まず今回作成するrect関数でX軸方向とY軸方向に絶対値をとり、四角形を描画します。

そして、三角関数のsin (サイン) とcos (コサイン) を使い、半円を描くような運動を実現しています。

サンプルの実行結果はコチラ

 

Fragment Shader

<script type="shader" id="fragment-shader">
uniform float time;
uniform vec2 resolution;

bool circle(vec2 position, float radius) {
    if(length(position) <= radius) {
        return true;
    } else {
        return false;
    }
}

bool rect(vec2 position, float width, float height) {
    if(abs(position.x) <= width && abs(position.y) <= height) {
        return true;
    } else {
        return false;
    }
}

void main() {
    vec2 position = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y);
    vec4 distColor = vec4(vec3(0.0), 1.0);

    //rect
    float rectW = 0.2;
    float rectH = 0.1;
    vec2 rectP = position * -1.0;
    rectP.x += cos(time);
    rectP.y += rectP.y;
    if(rect(rectP, rectW, rectH)) {
        vec4 inColor = vec4(0.0, 1.0 , 0.5, 1.0);
        distColor = inColor;
    }

    //circle
    float radius = 0.05;
    vec2 circleP = position * -1.0;
    circleP.x += cos(time);
    circleP.y += abs(sin(time)) + radius / 2.0 + rectH / 2.0;
    if(circle(circleP, radius)) {
        vec4 inColor = vec4(1.0, 0.0, 0.5, 1.0);
        distColor = inColor;
    } 
    gl_FragColor = distColor;
}
</script>

 

【応用編】BoxGeometryにオリジナルシェーダーを適用する

【応用編】BoxGeometryにオリジナルシェーダーを適用する

最後にBoxGeometryにシェーダーを適用してみたいと思います。

そのためには、Vertex Shaderの記述も必要になるので確認しておきましょう。

ちなみに、「varying」というのはFragment Shaderに値を送るという意味になります。

そしてFragment Shader側で送られてきた「vUv」を正規化して表面に模様を描画しています。

サンプルの実行結果はコチラ

 

Vertex Shader

<script type="shader" id="vertex-shader">
varying mat3 vNormalMatrix;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vPosition;

uniform float time;

void main() {
    vNormalMatrix = normalMatrix;
    vNormal = normal;
    vUv = uv;
    vPosition = position;

    gl_Position += projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0);
}
</script>

 

Fragment Shader

<script type="shader" id="fragment-shader">
varying mat3 vNormalMatrix;
varying vec3 vNormal;
varying vec2 vUv;
varying vec3 vPosition;

uniform float time;
uniform vec2 resolution;

void main(){
    vec2 position = -1.0 + 2.0 * vUv;
    vec4 distColor = vec4(0.0, 0.0, 0.0, 1.0);
    float color = floor(sin(length(position.y * 40.0) - time * 7.5) * 1.5);
    distColor = vec4(color, 0.0, color / 1.5, 1.0);
    gl_FragColor = distColor;
}
</script>

 

まとめ

今回は、Three.js入門者向けにオリジナルのシェーダーの実装方法について解説してきましたが、いかがでしたでしょうか?

GLSLはC言語のような書き方ですので、普段JavaScriptやPHPなどしか書いたことがない人にとっては少し扱いにくく感じるかもしれませんが、慣れてしまえばシンプルなコードだけで、非常にさまざまな絵を描画できることが分かってくるかと思います。

GLSLを自由に扱えるようになれば、Three.jsで高度な表現も可能になってくるのでフロントエンドのコーディングを極めたい人やコンピューターグラフィックに興味がある人にとっては、非常に良い学習材料になるはずです。

-JavaScript