2019年09月16日 - WebVR・Three.js

Blender2.8でglTFを出力

Blender2.8でglTFを出力

glTFをThree.jsで読み込み」でglTFをThree.jsで読み込みましたが、その頃は3DCGを始めたばかりでわからないことも多く、Three.jsで読み込んだとき色や質感が再現できませんでした。最近Blender2.8が正式リリースされ、標準でglTFが出力できるようになったので、「BlenderでiPhoneXSを制作(2)」で制作したiPhoneXSをglTFで出力して、Three.jsで読み込んでみました。

Blenderにも慣れてきて参考書も出始めてきたので、そろそろBlender2.8を使い始めようと思います。

Blender2.8でglTFを出力

● PBRマテリアルの設定

マテリアルとテクスチャを表示させるため、BlenderのプリンシプルBSDFでPBRマテリアルを設定します。PBR(物理ベースレンダリング)は現実の光学現象をシミュレートすることにより、リアルな質感を表現できるレンダリング方法です。

詳細は上記マニュアルに書いてありますが、プリンシプルBSDFを使用すると綺麗にglTFに出力することができます。
ただし、あくまでBlenderがglTFに変換してくれるということで、全ての設定に対応しているわけではありません(現時点ではベースカラー、メタリック、荒さ、ノーマルマップ、放射など)。テクスチャにも対応していますが、画像形式をPNGかJPGにする必要があります。

Blender2.79で制作したiPhoneXSをBlender2.8で開きます

Blender2.79で制作したiPhoneXSをBlender2.8で開きます。

選択したオブジェクトのみglTFに出力することもできますが、今回は他のオブジェクトは削除して、iPhoneXSのみglTFに出力します。

また、マテリアルは全てプリンシプルBSDFで設定します。

● glTFを出力

トップバーのglTF2.0をクリックします

Blender2.8で出力できるのはglTF2.0です。

トップバーの「ファイル > エクスポート > glTF2.0(.glb/.gltf)」をクリックします。

glTF2.0をエクスポートする画面が表示されます

glTF2.0をエクスポートする画面が表示されます。

モディファイアーを適用をチェックして、右上のglTF2.0をエクスポートをクリックしてglTFを出力します。

出力されるデータはシーン全体です。アニメーションを出力することもできます。

Three.jsでglTFを読みこみ

● Three.jsで読み込む

出力したglTFは、THREE.GLTFLoaderで読み込むことができます。glTFで読み込んだオブジェクトの影のつけ方は、「glTFをThree.jsで読み込み」を参照してください。

//glTFの読み込み
const gltfLoader = new GLTFLoader();

gltfLoader.load('./iphonexs.glb',function(data){
	const gltf = data;
	const obj = gltf.scene;
	scene.add(obj);
});

//読み込んだシーンが暗いので、明るくする
renderer.outputEncoding = THREE.GammaEncoding;

● MeshStandardMaterial

Three.jsでPBRマテリアルを使用したい場合は、MeshStandardMaterialを使用します

Three.jsでPBR(物理ベースレンダリング)を使用したい場合は、MeshStandardMaterialを使用します。

MeshStandardMaterialでテクスチャの他にノーマルマップを設定し、荒さ(roughness)を調整することで、アスファルトのデコボコを表現することができます。

● script.js

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

<script src="js/preloadjs.min.js"></script>
<script src="js/TweenMax.min.js"></script>

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

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

最近ChromeのAddEventListenerOptionsのpassiveがデフォルトでtrueになり、OrbitControlsを使用するとpreventDefaultが効かないため、スマホでタッチした時に意図せず画面がスクロールしてしまいます。そこで、passiveをfalseにする処理を追加しました。

document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});

基本的には「glTFをThree.jsで読み込み」と同じですが、完成したscript.jsです。

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

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

let scene,camera,renderer;
let orbitControls;
let textureArray;

function init(){
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,1000);
    camera.position.set(0,0,9);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.setClearColor(new THREE.Color(0x000000));
    renderer.physicallyCorrectLights = true;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    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(){
	TweenMax.to('.loader',0.1,{opacity:1});

	let gltfLoader = new GLTFLoader();
	gltfLoader.load('./data/iPhonexs.glb',
		function(gltf){
		const obj = gltf.scene;

		for(let i = 0; i < obj.children.length; i++){
			let mesh = obj.children[i];
			mesh.receiveShadow = true;
			mesh.castShadow = true;
		}
		scene.add(obj);
		obj.position.set(0.8,-1,0);
		obj.rotation.set(0,Math.PI * 0.025,0);
	});

	gltfLoader.load('./data/iPhonexs.glb',
		function(gltf){
		const obj = gltf.scene;

		for(let i = 0; i < obj.children.length; i++){
			let mesh = obj.children[i];
			mesh.receiveShadow = true;
			mesh.castShadow = true;
		}
		scene.add(obj);
		obj.position.set(-0.5,-1,-1.5);
		obj.rotation.set(0,Math.PI * 0.85,0);
	});

	const nameArray = ['texture','normalmap'];
	const manifestArray = [];

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

	const loadQueue = new createjs.LoadQueue();

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

	textureArray = [];

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

		for(let i = 0; i < nameArray.length; i++){
            let tempImage = loadQueue.getResult(nameArray[i]);
            let tempTexture = new THREE.Texture(tempImage);
            tempTexture.needsUpdate = true;
            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(){
	let loader = new THREE.TextureLoader();
	let texture,nrmTexture;
	let planeGeometry,planeMaterial,plane;

	for(let i=0; i<=3; i++){
		texture = textureArray[0];
		nrmTexture = textureArray[1];
		texture.wrapS = nrmTexture.wrapS = THREE.MirrorRepeatWrapping;
		texture.wrapT = nrmTexture.wrapT = THREE.MirrorRepeatWrapping;

		if(i==0){
			texture.repeat.set(2,2);
			nrmTexture.repeat.set(2,2);
			planeGeometry  = new THREE.PlaneGeometry(20,20);
		}else{
			texture.repeat.set(2,1);
			nrmTexture.repeat.set(2,1);
			planeGeometry  = new THREE.PlaneGeometry(20,10);
		}

		planeMaterial = new THREE.MeshStandardMaterial({
			map:texture,
			color: 0x666666,
			roughness : 0.6,
			normalMap: nrmTexture,
			normalScale: new THREE.Vector2( 2, -2),
			side:THREE.DoubleSide,
		});

		plane = new THREE.Mesh(planeGeometry,planeMaterial);
		plane.receiveShadow = true;

		if(i==0){
			plane.position.set(0,-1,0);
			plane.rotation.set(-Math.PI/2,0,0);
		}else if(i==1){
			plane.position.set(0,4,-10);
			plane.rotation.set(0,0,0);
		}else if(i==2){
			plane.position.set(10,4,0);
			plane.rotation.set(0,-Math.PI/2,0);
		}else if(i==3){
			plane.position.set(-10,4,0);
			plane.rotation.set(0,-Math.PI/2,0);
		}
		scene.add(plane);
	}

	renderer.outputEncoding = THREE.GammaEncoding;
}

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

	const positionArr = [
			[0,7,3,1],
			[-3,3.5,3,2.5],
			[3,3.5,-3,1]
		];

	for(let i = 0; i < positionArr.length; i++){
		let directionalLight = new THREE.DirectionalLight(0xCCCCCC, positionArr[i][3]);
		directionalLight.position.set( positionArr[i][0], positionArr[i][1], positionArr[i][2]);

		directionalLight.castShadow = true;
		directionalLight.shadow.camera.top = 50;
		directionalLight.shadow.camera.bottom = -50;
		directionalLight.shadow.camera.right = 50;
		directionalLight.shadow.camera.left = -50;
		directionalLight.shadow.mapSize.set(4096,4096);

		scene.add(directionalLight);

		//let helper = new THREE.DirectionalLightHelper( directionalLight, 1);
		//scene.add(helper);
	}
}

function setController(){
    document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false});
    orbitControls = new OrbitControls(camera,renderer.domElement);
    orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.5;
}

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

完成したデモになります。glTFをThree.jsで読み込むデモなので、パソコンとスマホで見ることができるようにしました。