Twitter
2019年10月21日 - WebVR・Three.js

Three.jsで海を制作

最近Blenderばかりで、ひさしぶりにThree.jsをさわりたくなったので、Three.jsのサンプルを参考に海を制作しました。

ECMAScript 2015(ES6)とThree.js

現在、IE11以外はほぼ対応が完了していて、ES Modulesを使用したかったので、ES6を使用します。ES6に関しては下記を参考にしてください。

Three.jsのES6に対応したライブラリデータは「Three.jsのフレームワークデータ」の中の「examples > jsm」に入っています。

● script.jsの読み込み

Three.js関連のライブラリはscript.jsからインポートするので、script.jsはtype="module"をつけて読み込みます。

<script src="js/script.js" type="module"></script>

● Three.jsのテンプレート

シーンやカメラ、レンダラーを毎回設定するのは面倒なので、基本的な設定をしたテンプレートを作成しました。

テンプレートは、確認のために座標軸とグリッドを表示しています。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';

//===============================================================
// Template
//===============================================================
window.addEventListener('load',function(){
   init();
});

let scene,camera,renderer;
let orbitControls;

function init(){
    //シーン、カメラ、レンダラーを生成
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,1000);
    camera.position.set(0.1,0,0);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);

    //OrbitControls
    document.addEventListener('touchmove',function(e){e.preventDefault();},{passive: false});
    const orbitControls = new OrbitControls(camera,renderer.domElement);

    //canvasを作成
    const container = document.querySelector('#canvas_vr');
    container.appendChild(renderer.domElement);

    //ウィンドウのリサイズに対応
    window.addEventListener('resize',function(){
        camera.aspect = window.innerWidth/window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth,window.innerHeight);
    },false);

    threeWorld();
    setLight();
    rendering();
}

function threeWorld(){
    //座標軸の生成
    const axes = new THREE.AxesHelper(1000);
    axes.position.set(0,0,0);
    scene.add(axes);
 
    //グリッドの生成
    const grid = new THREE.GridHelper(100,100);
    scene.add(grid);
}

function setLight(){
    //環境光
    const ambientLight = new THREE.AmbientLight(0xFFFFFF);
    scene.add(ambientLight);
}

function rendering(){
    requestAnimationFrame(rendering);
    renderer.render(scene,camera);
}

Three.jsで海の制作

● 海の制作

海は「examples > jsm > objects」にあるWater.jsを使用して制作できます。Water.jsは「examples > textures > water > Water_1_M_Normal.jpg」にあるノーマルマップを読みこむ必要があります。
また、Water.js内のthree.module.jsのパスを合わせる必要があります。

import { Water } from './lib/three_jsm/Water.js';

//PlaneGeometry
const waterGeometry = new THREE.PlaneGeometry(1000,1000);

//Water
water = new Water(
	waterGeometry,
	{
		textureWidth: 512,
		textureHeight: 512,
		waterNormals: new THREE.TextureLoader().load( './img/water/Water_1_M_Normal.jpg', function(texture){
			texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
		} ),
		alpha: 1.0,
		waterColor: 0x3e89ce,
		distortionScale: 3.7,
		fog:scene.fog !== undefined
	}
);

//シーンに追加
scene.add(water);
water.rotation.x = - Math.PI / 2;

function rendering(){
	//アニメーション
	water.material.uniforms['time'].value += 1.0 / 60.0;
}

● 空の制作

Water.jsを使用して海を制作しましたが、海だけだと不自然なので空も制作します。

空は「examples > jsm > objects」にあるSky.jsを使用して制作できます。また、Sky.js内のthree.module.jsのパスを合わせる必要があります。

import { Sky } from './lib/three_jsm/Sky.js';

//Sky
const sky = new Sky();
sky.scale.setScalar(450000);
scene.add(sky);

//Skyの設定
const sky_uniforms = sky.material.uniforms;
sky_uniforms['turbidity'].value = 10;
sky_uniforms['rayleigh'].value = 2;
sky_uniforms['luminance'].value = 1;
sky_uniforms['mieCoefficient'].value = 0.005;
sky_uniforms['mieDirectionalG'].value = 0.8;

//Sun
const sunSphere = new THREE.Mesh(
	new THREE.SphereGeometry(200,16,8),
	new THREE.MeshBasicMaterial({color:0xFFFFFF})
);
scene.add(sunSphere);

//Sunの設定
const sun_uniforms = sky.material.uniforms;
sun_uniforms['turbidity'].value = 10;
sun_uniforms['rayleigh'].value = 2;
sun_uniforms['mieCoefficient'].value = 0.005;
sun_uniforms['mieDirectionalG'].value = 0.8;
sun_uniforms['luminance'].value = 1;

const theta = Math.PI * ( -0.01 );
const phi = 2 * Math.PI * ( -0.25 );
const distance = 400000;
sunSphere.position.x = distance * Math.cos(phi);
sunSphere.position.y = distance * Math.sin(phi) * Math.sin(theta);
sunSphere.position.z = distance * Math.sin(phi) * Math.cos(theta);
sunSphere.visible = true;
sun_uniforms['sunPosition'].value.copy(sunSphere.position);

● 球体の制作

海と空を制作しましたが、せっかくなので泡のような球体も制作します。

まず「Learning 3D Graphics With Three.js | Dynamic Geometry」を参考に、頂点をアニメーションして泡のような柔らかさを表現します。そのさい、perlin.jsが必要なので読み込みます。

<script src="js/lib/perlin.js"></script>

次に「Three.jsでキューブ環境マッピング」でもやりましたが、海のHDRI画像を使用してキューブ環境マッピングで泡の映り込みを表現します。最後に「materials / shaders / fresnel」を参考にフレネル反射をつけました。

import { FresnelShader } from './lib/three_jsm/FresnelShader.js';

//Bubble
const geometry = new THREE.SphereGeometry(1,36,24);

const urls = [
	'./img/bubble/posx.jpg','./img/bubble/negx.jpg',
	'./img/bubble/posy.jpg','./img/bubble/negy.jpg',
	'./img/bubble/posz.jpg','./img/bubble/negz.jpg',
];

const textureCube = new THREE.CubeTextureLoader().load(urls);
textureCube.format = THREE.RGBFormat;

const shader = FresnelShader;
const uniforms = THREE.UniformsUtils.clone(shader.uniforms);
uniforms['tCube'].value = textureCube;
const material = new THREE.ShaderMaterial({
	uniforms:uniforms,
	vertexShader:shader.vertexShader,
	fragmentShader:shader.fragmentShader,
	blending:THREE.MultiplyBlending,
} );

sphere = new THREE.Mesh(geometry,material);
scene.add(sphere);

function rendering(){
	const time = performance.now() * 0.0005;
	const k = 1.5;

	for(let j = 0; j < sphere.geometry.vertices.length; j++){
		const p = sphere.geometry.vertices[j];
		p.normalize().multiplyScalar(3 + 0.15 * noise.perlin3(p.x * k + time, p.y * k, p.z * k));
	}
	sphere.geometry.computeVertexNormals();
	sphere.geometry.verticesNeedUpdate = true;
	sphere.geometry.normalsNeedUpdate = true;
}

● script.js

必要なライブラリを読み込みます。

<script src="js/preloadjs.min.js"></script>
<script src="js/TweenMax.min.js"></script>
<script src="js/lib/perlin.js"></script>
<script src="js/script.js" type="module"></script>

完成したscript.jsになります。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { Water } from './lib/three_jsm/Water.js';
import { Sky } from './lib/three_jsm/Sky.js';
import { FresnelShader } from './lib/three_jsm/FresnelShader.js';

//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
   init();
});

let scene,camera,renderer;
let orbitControls;
let water;
let sphereArr = [];
let spherePosArr = [];

function init(){
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,5000);
    camera.position.set(0,5,15);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.physicallyCorrectLights = true;

    const container = document.querySelector('#canvas_vr');
    container.appendChild(renderer.domElement);

    window.addEventListener('resize',function(){
        camera.aspect = window.innerWidth/window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth,window.innerHeight);
    },false);

    setLoading();
}

function setLoading(){
	const nameArray = ['water_texture','posx','posy','posz','negx','negy','negz'];
	const manifestArray = [];
	let path;

	TweenMax.to('.loader',0.1,{opacity:1});

	for(let i = 0; i < nameArray.length; i++){
        const name = nameArray[i];
        if(i==0){
        	path = './img/water/'+name+'.jpg';
        }else{
           	path = './img/bubble/'+name+'.jpg';
        }
        manifestArray.push({id:name,src:path})
    }

	const loadQueue = new createjs.LoadQueue();

	loadQueue.on('progress',function(e){
		const progress = e.progress;
	});

	const imageArray = [];
	const textureArray = [];

	loadQueue.on('complete',function(){

		for(let i = 0; i < nameArray.length; i++){
            const tempImage = loadQueue.getResult(nameArray[i]);
            const tempTexture = new THREE.Texture(tempImage);
            tempTexture.needsUpdate = true;
            imageArray.push(tempImage);
            textureArray.push(tempTexture);
        }

		TweenMax.to('#loader_wrapper',1,{
	        opacity:0,
	        delay:1,
	        onComplete: function(){
	            document.getElementById('loader_wrapper').style.display ='none';
	        }
	    });
        threeWorld();
		setLight();
		setController();
		rendering();
	});

	loadQueue.loadManifest(manifestArray);
}

function threeWorld(){
	const waterGeometry = new THREE.PlaneGeometry(1000,1000);
	water = new Water(
		waterGeometry,
		{
			textureWidth: 512,
			textureHeight: 512,
			waterNormals: new THREE.TextureLoader().load('./img/water/water_texture.jpg',function(texture){
				texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
			} ),
			alpha: 0.6,
			waterColor: 0x3e89ce,
			distortionScale: 3.7,
			fog:scene.fog !== undefined
		}
	);
	water.rotation.x = - Math.PI / 2;
	water.position.y = -5;
	scene.add(water);

	const sky = new Sky();

	const sky_uniforms = sky.material.uniforms;
	sky_uniforms['turbidity'].value = 10;
	sky_uniforms['rayleigh'].value = 2;
	sky_uniforms['luminance'].value = 1;
	sky_uniforms['mieCoefficient'].value = 0.005;
	sky_uniforms['mieDirectionalG'].value = 0.8;
	sky.scale.setScalar(450000);
	scene.add(sky);

	const sunSphere = new THREE.Mesh(
		new THREE.SphereGeometry(200,16,8),
		new THREE.MeshBasicMaterial({color:0xFFFFFF})
	);

	const sun_uniforms = sky.material.uniforms;
	sun_uniforms['turbidity'].value = 10;
	sun_uniforms['rayleigh'].value = 0.3;
	sun_uniforms['mieCoefficient'].value = 0.005;
	sun_uniforms['mieDirectionalG'].value = 0.8;
	sun_uniforms['luminance'].value = 0.8;

	const theta = Math.PI * (-0.2);
	const phi = 2 * Math.PI * (-0.25);
	const distance = 4000;
	sunSphere.position.x = distance * Math.cos(phi);
	sunSphere.position.y = distance * Math.sin(phi) * Math.sin(theta);
	sunSphere.position.z = distance * Math.sin(phi) * Math.cos(theta);
	sunSphere.visible = true;
	sun_uniforms['sunPosition'].value.copy( sunSphere.position );

	const bubble_geometry = new THREE.SphereGeometry(1,36,24);

	const urls = [
		'./img/bubble/posx.jpg','./img/bubble/negx.jpg',
		'./img/bubble/posy.jpg','./img/bubble/negy.jpg',
		'./img/bubble/posz.jpg','./img/bubble/negz.jpg',
	];

	const textureCube = new THREE.CubeTextureLoader().load(urls);
	textureCube.format = THREE.RGBFormat;

	const shader = FresnelShader;
	const uniforms = THREE.UniformsUtils.clone(shader.uniforms);
	uniforms['tCube'].value = textureCube;
	const bubble_material = new THREE.ShaderMaterial({
		uniforms: uniforms,
		vertexShader: shader.vertexShader,
		fragmentShader: shader.fragmentShader,
		blending:THREE.MultiplyBlending,
	});

	spherePosArr = [];

	for(let i = 0; i < 25; i++){
		const sphere = new THREE.Mesh(bubble_geometry,bubble_material);
		sphere.position.x = Math.random() * 50 - 25;
		sphere.position.y = Math.random() * 10 - 3;
		sphere.position.z = Math.random() * 50 - 25;
		sphere.rotation.y = Math.random() * Math.PI / 2;
		sphere.rotation.z = Math.random() * Math.PI / 2;
		sphere.scale.x = sphere.scale.y = sphere.scale.z = Math.random() * 1.5 + 0.5;
		scene.add(sphere);
		sphereArr.push(sphere);
		spherePosArr.push(sphere.position.y);
	}
}

function setLight(){
	const ambientLight = new THREE.AmbientLight(0xFFFFFF);
	scene.add(ambientLight);
}

function setController(){
	document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});
	const orbitControls = new OrbitControls(camera,renderer.domElement);
	orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.5;
	orbitControls.maxPolarAngle = Math.PI * 0.495;
	orbitControls.minDistance = 5.0;
	orbitControls.maxDistance = 25.0;
}

function rendering(){
	if(orbitControls){
        orbitControls.update();
    }

    const time = performance.now() * 0.0005;
	const time2 = performance.now() * 0.0003;
	let anim_time;

	const k = 1.5;

	for(let i=0; i < sphereArr.length; i++){
		let sphere = sphereArr[i];

		for(let j = 0; j < sphere.geometry.vertices.length; j++){
			const p = sphere.geometry.vertices[j];
			p.normalize().multiplyScalar(2 + 0.15 * noise.perlin3(p.x * k + time, p.y * k, p.z * k));
		}
		sphere.geometry.computeVertexNormals();
		sphere.geometry.verticesNeedUpdate = true;
		sphere.geometry.normalsNeedUpdate = true;

		if(i % 2 == 0){
			anim_time = time;
		}else{
			anim_time = time2;
		}
		sphere.position.y = Math.sin(anim_time) * 2.5 + spherePosArr[i];
	}

	water.material.uniforms['time'].value += 1.0 / 240.0;

	requestAnimationFrame(rendering);
    renderer.render(scene,camera);
}

完成したデモになります。Three.jsを試したかったのとHMDには負荷がかかりすぎると思ったのでパソコンとスマホで見ることができるようにしました。

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

Three.jsでglTFアニメーション

次の記事へ

Three.jsで煙を制作