カテゴリー別アーカイブ: WebGL

Three.js でレンズフレア(THREE.LensFlare)を実装してみる


Three.js にはいろいろと役に立つクラスが用意されています。今回はそのうちの一つである THREE.LensFlare クラスについて紹介したいと思います。写真や映像に太陽等を写した時に印象的な光の模様が写っていることがあります。これをレンズフレアと言いますが、これを簡単に実現するためのクラスが THREE.LensFlare クラスです。

レンズフレアの素材を準備する

レンズフレア一つ一つの素になる画像を用意します。今回は以下のような 3 種類の画像を Gimp を使って作成しました。


それっぽい素材を作るコツとしては、同一形状で統一することです。今回のように丸なら丸だけ、六角形なら六角形だけというようにします。これは元々レンズフレアがレンズの性質によって発生するもので、同じレンズなら同じ形状のレンズフレアが発生するという仕組みがあるためです。

THREE.LensFlare

さっそく実装してみたコードを見てみましょう。 67 行目からがレンズフレアを作成している部分です。
69 行目から 71 行目で先ほど作成した画像ファイルをテクスチャとしてロードしています。
次に 73 行目で THREE.LensFlare クラスを使ってレンズフレアになる大元のオブジェクトを作成しています。引数は順番に

  • テクスチャ
  • 描画サイズ(-1 とすることでテクスチャのサイズになる)
  • 光源からの距離(0 から 1)
  • ブレンディング方法(光なので加算ブレンドが有効でしょう)

となっています。

次から作成した大元のオブジェクトに子のフレアを add メソッドで追加していきます。引数の最後に透明度の指定が追加されているだけで他は先ほどと同じです。

最後に 80 行目でシーンに追加しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<div id="div_canvas_1194" style="text-align:center;">
<script>
var camera, scene, renderer;
 
init();
initScene();
initLight();
initLensFlare();
animate();
 
function init() {
    scene = new THREE.Scene();
 
    camera = new THREE.PerspectiveCamera( 75, 1, 1, 10000 );
    camera.position.z = 1000;
    scene.add( camera );
 
    div_canvas = document.getElementById( 'div_canvas_1194' );
    renderer = new THREE.WebGLRenderer();
    renderer.setSize( 500, 500 );
	renderer.setClearColorHex( 0x000000, 1 );
 
    div_canvas.appendChild( renderer.domElement );
 
    trackball = new THREE.TrackballControls( camera, renderer.domElement );
}
 
function initScene() {
    material_green = new THREE.MeshLambertMaterial( { color: 0x00ff00, ambient: 0x00ff00, shading: THREE.FlatShading } );
    material_red = new THREE.MeshLambertMaterial( { color: 0xff0000, ambient: 0xff0000, shading: THREE.FlatShading } );
    material_blue = new THREE.MeshLambertMaterial( { color: 0x0000ff, ambient: 0x0000ff, shading: THREE.FlatShading } );
    material_white = new THREE.MeshLambertMaterial( { color: 0xffffff, ambient: 0xffffff, shading: THREE.FlatShading } );
 
    cubeGeometry = new THREE.CubeGeometry( 100, 100, 100 );
    cubeMesh = new THREE.Mesh( cubeGeometry, material_green );
    cubeMesh.position.y = 200;
    scene.add( cubeMesh );
 
    sphereGeometry = new THREE.SphereGeometry( 100, 16, 16 );
    sphereMesh = new THREE.Mesh( sphereGeometry, material_red );
    sphereMesh.position.x = -200;
    sphereMesh.position.y = 200;
    scene.add( sphereMesh );
 
    octahedronGeometry = new THREE.OctahedronGeometry( 100, 2 );
    octahedronMesh = new THREE.Mesh( octahedronGeometry, material_blue );
    octahedronMesh.position.x = 200;
    octahedronMesh.position.y = 200;
    scene.add( octahedronMesh );
 
    planeGeometry = new THREE.PlaneGeometry( 1000, 1000, 15, 15 );
    planeMesh = new THREE.Mesh( planeGeometry, material_white );
    planeMesh.position.y = -100;
    planeMesh.rotation.x = -90 * 2 * Math.PI / 360;
    scene.add( planeMesh );
}
 
function initLight()
{
	point_light = new THREE.PointLight( 0xffffff, 1, 0 );
	scene.add( point_light );
 
	ambient_light = new THREE.AmbientLight( 0x444444 );
	scene.add( ambient_light );
}
 
function initLensFlare()
{
    texture_00 = THREE.ImageUtils.loadTexture( "http://www.soft-syokunin.com/wp-content/uploads/lens_flare00.png" );
    texture_01 = THREE.ImageUtils.loadTexture( "http://www.soft-syokunin.com/wp-content/uploads/lens_flare01.png" );
    texture_02 = THREE.ImageUtils.loadTexture( "http://www.soft-syokunin.com/wp-content/uploads/lens_flare02.png" );
 
    lens_flare = new THREE.LensFlare( texture_02, 16, 0, THREE.AdditiveBlending, new THREE.Color( 0xffffff ) );
    lens_flare.add( texture_02, 32, 0.2, THREE.AdditiveBlending, new THREE.Color( 0xffffff ), 0.3 );
    lens_flare.add( texture_01, 32, 0.4, THREE.AdditiveBlending, new THREE.Color( 0xffffff ), 0.3 );
    lens_flare.add( texture_02, 48, 0.5, THREE.AdditiveBlending, new THREE.Color( 0xffffff ), 0.2 );
    lens_flare.add( texture_00, 64, 0.7, THREE.AdditiveBlending, new THREE.Color( 0xffffff ), 0.3 );
    lens_flare.add( texture_00, 96, 1, THREE.AdditiveBlending, new THREE.Color( 0xffffff ), 0.4 );
 
    scene.add( lens_flare );
}
 
function animate() {
    // note: three.js includes requestAnimationFrame shim
    requestAnimationFrame( animate );
    render();
}
 
function render() {
    trackball.update();
    renderer.render( scene, camera );
}
</script>
</div>

動作と実装

上記のソースコードの動作例が以下です。右ドラックでカメラを移動させたあとにカメラを動かすとそれと連動してレンズフレアが動きます。これだけ簡単に実装出来るだけでも驚きですが、けっこう動きも凝っています。例えば光源を遮るようにオブジェクトが重なるとレンズフレアも薄くなります。

この辺の実装を見る場合は

  • three.js/src/extras/plugins/LensFlarePlugin.js
  • three.js/src/extras/shaders/ShaderFlares.js

を参考にすると良いでしょう。シェーダを上手に使って実現しているのが分かります。

Three.js でオリジナルシェーダを書く


Three.js には豊富なシェーダも付属していますが、オリジナルのシェーダを書くことも出来ます。開発段階では付属のシェーダを使うのがお手軽ですが、見た目を追及するにはシェーダに手を入れる必要も出てきます。ということで、 Three.js でのオリジナルシェーダの書き方を調べてみました。

今回は次のサイトを参考にしました。
SHAGERS-part2-

ShaderMaterial

THREE.ShaderMaterial を使うとオリジナルシェーダを書くことが出来ます。上記のサイトでは THREE.MeshShaderMaterial を使っていますが、現状は内部的に同じものになっています。

26 行目からの initShaderMaterial() 内で ShaderMaterial を作成し、あとで作る Mesh に適用しています。 ShaderMaterial を作成しているところに注目すると、パラメータとして

  • 頂点シェーダ
  • フラグメントシェーダ
  • uniform 変数
  • attribute 変数

を渡しています。今回は頂点がゆらゆら揺れるような頂点シェーダを書きました。フラグメントシェーダは赤色を渡すだけの単純なものです。 uniform 変数は頂点シェーダで使うための 0 から 2π まで変化する値を渡しています。 attribute 変数は空です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
  <div id="div_canvas">
	<script>
var camera, scene, renderer;
 
init();
initShaderMaterial();
initScene();
animate();
 
function init() {
    scene = new THREE.Scene();
 
    camera = new THREE.PerspectiveCamera( 75, 1, 1, 10000 );
    camera.position.z = 1000;
    scene.add( camera );
 
    div_canvas = document.getElementById( 'div_canvas' );
    renderer = new THREE.WebGLRenderer();
    renderer.setSize( 500, 500 );
 
    div_canvas.appendChild( renderer.domElement );
 
    trackball = new THREE.TrackballControls( camera, renderer.domElement );
}
 
function initShaderMaterial() {
	uniforms = {
		fSinTime0_X: {
			type: 'f',
			value: 0
		}
	};
 
	attributes = {
	};
 
	shader_material = new THREE.ShaderMaterial( {
		wireframe: true,
		vertexShader: [ "uniform float fSinTime0_X;",
						"void main(void)",
						"{",
						"   vec3 pos = position;",
						"   pos.x = pos.x + 10.0 * cos( pos.x * fSinTime0_X );",
						"   pos.y = pos.y + 10.0 * sin( pos.y * fSinTime0_X );",
						"   ",
						"   gl_Position = projectionMatrix * viewMatrix * vec4( pos, 1.0 );",
						"}" ].join( "\n" ),
		fragmentShader: [ "void main(void)",
						  "{",
						  "   gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );",
						  "}"
						].join( "\n" ),
		uniforms: uniforms,
		attributes: attributes
	} );
}
 
function initScene() {
    sphereGeometry = new THREE.SphereGeometry( 200, 8, 8 );
    sphereMesh = new THREE.Mesh( sphereGeometry, shader_material );
    scene.add( sphereMesh );
}
 
var angle = 0;
function animate() {
    // note: three.js includes requestAnimationFrame shim
	uniforms.fSinTime0_X.value = angle * 2 * Math.PI / 360;
 
	angle += 0.1;
	if( angle > 360 ) {
		angle = angle - 360;
	}
 
    requestAnimationFrame( animate );
    render();
}
 
function render() {
    trackball.update();
    renderer.render( scene, camera );
}
	</script>
  </div>

uniform 変数

uniform 変数はアプリケーションからシェーダに渡される読み込み専用の値が格納された変数です。uniform 変数は次のようなオブジェクト配列で指定します。キーになっている uniform 変数名で頂点シェーダから参照します。

uniforms = {
	uniform 変数名: {
		type:,
		value:}
};

また “type” の種類と “value” に入れる値の一覧は次の通りです。

typeサイズvalue
i1int
f1float
vec22THREE.Vector2
vec33THREE.Vector3
vec44THREE.Vector4
c3THREE.Color

暗黙に渡される変数

さて、シェーダを書いた経験のある方なら分かると思うのですが、本来であればもっと色々な変数を渡す必要があるはずです。例えば、頂点シェーダで使っている position や projectionMatrix、viewMatrix は uniform 変数や attribute 変数の指定もしていません。かといって、これらの指定をしなくても良いわけではなく、 Three.js が暗黙のうちに渡しているのです。

どこでやっているかというと、 tree.js/src/renderers/WebGLRenderer.js の 5094 行目あたりに prefix_vertex という変数があり、この文字列が 5206 行目あたりで渡した頂点シェーダに結合されています。(フラグメントシェーダも同様です)

代表的なものを抜き出すと以下のような物があります。役割は名前から大体見当がつくかと思いますので割愛します。これらに関しては自前の頂点シェーダから省略出来るということですね。それにしてもいくらか量があるのでオーバーヘッドがないのか気になります。

変数タイプ変数名
uniformmat4modelViewMatrix
uniformmat4projectionMatrix
uniformmat4viewMatrix
uniformvec3cameraPosition
uniformmat4cameraInverseMatrix
attributevec3position
attributevec3normal
attributevec2uv

Three.js でカメラコントロール(TrackballControls)を使う


3D プログラミングをしていると、シーンのプレビュー等でマウスでカメラをコントロールしたくなる場面があります。ですが、一からやろうとするとなかなか面倒くさい!こんな時にも Three.js には便利な仕組みがあります。

TrackballControls

それが TrackballControls というクラスです。 three.js/src/extras/controls にあります。使い方はものすごく簡単です。今回使ったソースを元に説明します。

カメラのコントロールが出来ても見る対象がないと動いているか分かりませんので、まずシーンを構築します。といっても、適当にオブジェクトを作って配置をしているだけです。(27 行目)

24 行目で TrackballControls オブジェクトを作成しています。第 1 引数にカメラオブジェクト、第 2 引数にレンダリング対象の DOM オブジェクトを指定しています。 対象の DOM に対するマウスイベントを取るようにしているわけですね。

あとはレンダリングループで update メソッドを呼び出しているだけです。(59 行目)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<div id="div_canvas" style="text-align:center;">
<script>
var camera, scene, renderer;
 
init();
initScene();
animate();
 
function init() {
    scene = new THREE.Scene();
 
    camera = new THREE.PerspectiveCamera( 75, 1, 1, 10000 );
    camera.position.z = 1000;
    scene.add( camera );
 
    material = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } );
 
    div_canvas = document.getElementById( 'div_canvas' );
    renderer = new THREE.WebGLRenderer();
    renderer.setSize( 500, 500 );
 
    div_canvas.appendChild( renderer.domElement );
 
    trackball = new THREE.TrackballControls( camera, renderer.domElement );
}
 
function initScene() {
    cubeGeometry = new THREE.CubeGeometry( 100, 100, 100 );
    cubeMesh = new THREE.Mesh( cubeGeometry, material );
    cubeMesh.position.y = 200;
    scene.add( cubeMesh );
 
    sphereGeometry = new THREE.SphereGeometry( 100, 8, 8 );
    sphereMesh = new THREE.Mesh( sphereGeometry, material );
    sphereMesh.position.x = -200;
    sphereMesh.position.y = 200;
    scene.add( sphereMesh );
 
    octahedronGeometry = new THREE.OctahedronGeometry( 100, 1 );
    octahedronMesh = new THREE.Mesh( octahedronGeometry, material );
    octahedronMesh.position.x = 200;
    octahedronMesh.position.y = 200;
    scene.add( octahedronMesh );
 
    planeGeometry = new THREE.PlaneGeometry( 1000, 1000, 10, 10 );
    planeMesh = new THREE.Mesh( planeGeometry, material );
    planeMesh.position.y = -100;
    planeMesh.rotation.x = 90 * 2 * Math.PI / 360;
    scene.add( planeMesh );
}
 
function animate() {
    // note: three.js includes requestAnimationFrame shim
    requestAnimationFrame( animate );
    render();
}
 
function render() {
    trackball.update();
    renderer.render( scene, camera );
}
</script>
</div>

操作方法

あっという間にカメラコントロールが出来てしまいました。操作方法は

  • 左ドラッグで注視点を中心にした移動
  • 中ドラッグで前後移動
  • 右ドラッグで平行移動

に対応しています。

その他、各種パラメータでカメラの挙動を調整することができます。カメラ制御の勉強がてらソースコードを読んでみると良いかと思います。