본문 바로가기
코딩과 알고리즘

3차원 웹, 땅아 드러나라!, 캔버스와 함께하는 자바스크립트, 21번째 시간

하나님이 이르시되 천하의 물이 한 곳으로 모이고

뭍이 드러나라 하시니 그대로 되니라

하나님이 뭍을 땅이라 부르시고

모인 물을 바다라 부르시니

하나님이 보시기에 좋았더라

- 성서 창세기 1장 중에서 -

슈퍼북이라는 해외 컨텐츠 앞부분에 '천지창조' 영상이 나오는데 아주 끝내줍니다.

한번 감상하시고 가보시면 어떨른지요? :) 참고로 슈퍼북 공식 유튜브 사이트 공개 방식에 따라 링크되었습니다.

https://youtu.be/fn-wEOpPsMo

불러오는 중입니다...

 


 

이번 시간에는 지난 시간에 이어 바다 위에 섬을 띄우는 방법을 다뤄보도록 하겠습니다.

섬의 모양은 블렌더에서 툴을 이용해서 비교적 쉽게 작업했는데요.

역시 크레이가 제공하는 모델을 사용하면 되지만, 직접 블렌더로 만들어보실 분을 위해 약간의 방법을 공유해드리겠습니다.

노파심에 말씀드리지만, 이 아래 부분은 스크립트와는 관련이 없는 부분이니 전혀 모르시더라도 낙심하지 않으셔도 됩니다 :)

 


블렌더 2.79버전에는 아주 멋진 지형도구가 들어 있습니다.

다른 버전은 사용을 안해봐서 모르겠습니다.

보통 기본으로는 사용할 수가 없고 add-on 을 이용해 주셔야 합니다.

블렌더 실행후 Ctrl + Alt + U 키를 누르면 환경설정 창이 나오는데,

애드온(Add-on)탭 선택 후

쪽에 land 까지만 타이핑 하고 Enter 키를 차면 add-on 기능이 딱 하나 등장할 겁니다.

체크상자를 체크하면 Add-on 기능이 활성화 되고

왼쪽 아래 "사용자 설정을 저장" 버튼을 선택하면 이 후로는 블렌더를 종료했다 다시 켜도 landscape 기능이 자동으로 시작됩니다.

landscape 기능은 어떻게 사용하는 걸까요?

아주 간단합니다. 뷰어 스크린에서 Shift+A 키 누르고,

메쉬 메뉴로 가시면 Landscape 를 선택하실 수 있습니다.

그러면 기본으로 산이 하나 생겨납니다.

이 모양은 처음 만들때 언제나 똑같은데요.

다양한 산을 만들 수 있는 툴은 왼쪽 아래에 있습니다.

화면을 위로 키워보시면 엄청난 옵션량을 확인해보실 수 있는데요.

주의깊게 보실 부분은 '노이즈'와 '랜덤씨드'입니다.

노이즈는 기본으로 '헤테로 터레인'으로 되어 있는데요.

뭐 지금 나온 지형 스타일이 마음에 든다면야 상관없지만,

지형으로 가장 어울리는 옵션은 따로 있습니다.

바로 'Rigged MFractal'입니다.

다른 스타일은 뭐 한번 보셔도 좋겠지만,

크레이는 이 스타일이 제일 나은것 같습니다.

그리고 랜덤씨드 0값은 약간 덜 예뻐 보이긴 하는데 값을 다른 값으로 바꿔봅시다.

랜덤 씨드값을 2로 바꿔볼까요?

바꿀 때마다 전혀 새로운 지형이 짜잔~ 하고 탄생합니다.

9701을 한번 넣어볼까요? 이 것도 꽤 멋지군요.

오프셋X값을 조정하면,

산맥이 마치 물결이 흘러가듯 꿈틀거리며 이동합니다.

오프셋Y값도 마찬가지지요.

이건 마치 산맥을 타고 여행하는 느낌입니다 :)

이런 식으로 마음에 드는 지형을 찾아나서는 겁니다.

그러다가 적당한 지형을 만나면 그걸 사용하면 되는 거지요.

UV맵을 펴서 텍스쳐를 입히는 부분은 굳이 생략하도록 하겠습니다.

파일 - 내보내기 - 콜라다 메뉴를 선택해주시면 됩니다.

중요한 것은 이부분을 꼭 체크해주셔야지,

안 해 주시면 빛과 카메라까지 출력되어 제대로 텍스쳐가 입혀지지 않는 불상사가 일어날겁니다 :)

이렇게 저장한 파일을 island 섬 모델파일로 활용하면 되는 거지요.


이제 다시 자바스크립트로 돌아와 시작하도록 하겠습니다.

집 모양을 불러오는 model 변수를 정의한 곳 아랫 부분에,

var model;
       :

아래 소스를 넣어보도록 하겠습니다.

var land;

그리고 집 모양의 모델을 불러오는 아랫 부분에,

	model.position.set(0,3,0);
	scene.add( model );
});
   :

다음 소스를 넣도록 하겠습니다.

loaderMesh.load(
	'http://dreamplan7.cafe24.com/canvas/dae/island2.dae',
	function ( collada ){
		land = collada.scene;
		loader.load(
			'http://dreamplan7.cafe24.com/canvas/dae/rock05.jpg', 
			function ( texture ) {
				land.children[0].children[0].material = new THREE.MeshStandardMaterial({map: texture});
				land.children[0].children[0].castShadow=true;
			}
		);
		land.scale.set(300,300,300);
		land.rotation.x= -90 * ( Math.PI / 180 );
		land.position.set(0,-50,50);
		scene.add( land );
});

소스를 설명드리자면 처음에는 섬의 모델링 파일을 불러오는과정이 진행되지요.

loaderMesh.load(

    'http://dreamplan7.cafe24.com/canvas/dae/island2.dae',

    function ( collada ){

        land = collada.scene;

참고로 http://dreamplan7.cafe24.com/canvas/dae/island2.dae 파일은 크레이가 준비한 섬 모델 파일입니다.

만일 앞 과정에서 블렌더로 다른 섬 모델링 파일을 준비하신 분은 서버에 업로드한 다음 해당 파일을 사용해 주셔도 좋을 듯합니다 :)

그리고 섬 전체를 덮어줄 텍스쳐 파일을 로딩하는데요.

loader.load(

'http://dreamplan7.cafe24.com/canvas/dae/rock05.jpg',

function ( texture ) {

land.children[0].children[0].material = new THREE.MeshStandardMaterial({map: texture});

land.children[0].children[0].castShadow=true;

}

);

그림 파일이 이렇게 생겼습니다. 섬 전체를 덮어도 그대로 느낌이 살아나는 멋진 텍스쳐더라구요 :)

참고로 위 파일은 https://www.turbosquid.com 사이트에서 제공하는 무료 텍스쳐입니다.

그리고 이어서 크기를 조정합니다.

land.scale.set(300,300,300);

섬의 크기는 처음에 1x1x1 기준으로 매우 조그만 미니섬입니다.

이 섬을 300 배 확대를 하는 거지요.

그런 다음 위치를 조정하고,

land.position.set(0,-50,50);

각도를 90도 회전하는 겁니다.

land.rotation.x= -90 * ( Math.PI / 180 );

각도를 회전 안하면 어떤 일이 일어나냐구요?

아래와 같은 사태가 벌어지지요 :)

뭐 나름대로 멋있긴 하지만 우리가 원하는 건 이게 아니지요 ㅎ..

그래서 결국 아래와 같은 장면이 연출되었습니다.

산맥은 약간 의도해서 가운데가 움푹 파인형태가 되도록 하였습니다.

만약 블렌더를 통해 자신만의 섬을 만드신 다면

이런 섬도 만들 수 있겠지요 :)

스크립트 수정은 이제 전부입니다. 이번 회차 코딩량의 수정은 아주 적은 정도이지만,

결과는 꽤 만족스러운 듯 한데 여러분도 그런지 모르겠습니다 :)

전체 소스 게제합니다. 소스가 점검 길어지는 군요.

다음 회차에서는 소스를 구조적으로 나누면서 조정하는 부분을 다뤄보도록 하겠습니다.

읽어주시느라 수고하셨습니다 :)

<html>
	<head>
		<title>3차원 캔바스 예제 1</title>
		<style>
			body { margin: 0; }
			canvas { width: 100%; height: 100% }
		</style>
	</head>
	<body>
		<script src="https://threejs.org/build/three.min.js"></script>
		<script src="http://fenixrepo.fao.org/cdn/js/threejs/4.4/OrbitControls.js"></script>
		<script src="https://cdn.rawgit.com/mrdoob/three.js/r69/examples/js/loaders/ColladaLoader.js"></script>
		<script src="http://dreamplan7.cafe24.com/canvas/js/Sky.js"></script>		
		<script src="http://dreamplan7.cafe24.com/canvas/js/Water.js"></script>		

		<script>

			// ==========================
			// 초기화 부분 시작 ( 이 부분은 문서에서 한번만 수행되면 됩니다 )
			// ==========================

			// 3차원 세계
			var scene = new THREE.Scene();

			// 카메라 ( 카메라 수직 시야 각도, 가로세로 종횡비율, 시야거리 시작지점, 시야거리 끝지점
			var camera = new THREE.PerspectiveCamera( 50, window.innerWidth/window.innerHeight, 1, 20000 );

			// 렌더러 정의 및 크기 지정, 문서에 추가하기
			var renderer = new THREE.WebGLRenderer( { antialias: true, preserveDrawingBuffer: true } );
			renderer.setSize( window.innerWidth, window.innerHeight );
			document.body.appendChild( renderer.domElement );
			renderer.shadowMapEnabled = true;
			renderer.shadowMap.type = THREE.PCFShadowMap;		// <-- 속도가 빠르다
			renderer.gammaInput = true;
			renderer.gammaOutput = true;

			var model;
			var land;

			var loader = new THREE.TextureLoader();
			var loaderMesh = new THREE.ColladaLoader();

			loaderMesh.load(
				'http://dreamplan7.cafe24.com/canvas/img/home.dae',
				function ( collada ){						
					model = collada.scene;
					loader.load(
						'http://dreamplan7.cafe24.com/canvas/img/checkPattern.jpg', 
						function ( texture ) {
							model.children[0].children[0].material = new THREE.MeshStandardMaterial({color: 0xf0f0f0});
							model.children[1].children[0].material = new THREE.MeshStandardMaterial({color: 0xf0f0f0});
							model.children[2].children[0].material = new THREE.MeshStandardMaterial({color: 0xf0f0f0});
							model.children[3].children[0].material = new THREE.MeshStandardMaterial({map: texture});
							model.children[4].children[0].material = new THREE.MeshStandardMaterial({map: texture});
							model.children[5].children[0].material = new THREE.MeshStandardMaterial({map: texture});
						
							model.children[0].children[0].castShadow=true;
							model.children[1].children[0].castShadow=true;
							model.children[2].children[0].castShadow=true;
							model.children[3].children[0].castShadow=true;
							model.children[4].children[0].castShadow=true;
							model.children[5].children[0].castShadow=true;
						}
					);

					model.rotation.x= -90 * ( Math.PI / 180 ); 
					model.rotation.z= -90 * ( Math.PI / 180 ); 
					model.position.set(0,3,0);
					scene.add( model );
      });

			loaderMesh.load(
				'http://dreamplan7.cafe24.com/canvas/dae/island2.dae',
				function ( collada ){
					land = collada.scene;
					loader.load(
						'http://dreamplan7.cafe24.com/canvas/dae/rock05.jpg', 
						function ( texture ) {
							land.children[0].children[0].material = new THREE.MeshStandardMaterial({map: texture});
							land.children[0].children[0].castShadow=true;
						}
					);
					land.scale.set(300,300,300);
					land.position.set(0,-50,50);
					land.rotation.x= -90 * ( Math.PI / 180 );
					
					scene.add( land );
      });

			// 바닥
			var floor;
			floor = new THREE.Mesh(
				new THREE.BoxGeometry(10, 10, 10)
			);
			loader.load(
					'http://dreamplan7.cafe24.com/canvas/img/floor1.jpg', 
					function ( texture ) {
						floor.material = new THREE.MeshStandardMaterial({map: texture});
						floor.material.map.repeat.x=3;
						floor.material.map.repeat.y=3;
						floor.material.map.wrapS=THREE.RepeatWrapping;
						floor.material.map.wrapT=THREE.RepeatWrapping;
					}
			);
			scene.add(floor);

			floor.position.set(0, -3, 0);
			floor.receiveShadow=true;

			// 카메라의 위치 조정
			camera.position.set ( 25, 5, 3 );
	
			// 카메라가 회전하는
			var controls = new THREE.OrbitControls (camera, renderer.domElement);
			controls.enablePan = false;
			controls.minPolarAngle = Math.PI / -2;
			controls.maxPolarAngle = Math.PI / 2.1;

			// 전체 조명을 추가합니다.
			var light_base = new THREE.AmbientLight( 0xf0f0f0 ); // soft white light
			scene.add( light_base );

			var light_sun = new THREE.DirectionalLight ( 0x808080, 5.0 );
			//light_sun.position.set( 200, 200, 300 );
			scene.add( light_sun );
			shadowBlur=10;
			light_sun.castShadow=true;
			light_sun.shadowCameraLeft=-shadowBlur;
			light_sun.shadowCameraRight=shadowBlur;
			light_sun.shadowCameraTop=shadowBlur;
			light_sun.shadowCameraBottom=-shadowBlur;

			// Water
			var waterGeometry = new THREE.PlaneBufferGeometry( 100000, 100000 );

			water = new THREE.Water(
				waterGeometry,
				{
					textureWidth: 512,
					textureHeight: 512,
					waterNormals: new THREE.TextureLoader().load( 'http://dreamplan7.cafe24.com/canvas/img/waternormals.jpg', function ( texture ) {

						texture.wrapS = texture.wrapT = THREE.RepeatWrapping;

					} ),
					alpha: 1.0,
					sunDirection: light_sun.position.clone().normalize(),
					sunColor: 0xffffff,
					waterColor: 0x001e0f,
					distortionScale: 3.7,
					fog: scene.fog !== undefined
				}
			);

			water.rotation.x = - Math.PI / 2;
			scene.add( water );

			var sky = new THREE.Sky();

			sky.material.uniforms['turbidity'].value=10;
			sky.material.uniforms['rayleigh'].value=2;
			sky.material.uniforms['luminance'].value=1;
			sky.material.uniforms['mieCoefficient'].value=0.005;
			sky.material.uniforms['mieDirectionalG'].value=0.8;

			var parameters = {
				distance: 400,
				inclination: 0.1,
				azimuth: 0.05
			};

			var cubeCamera = new THREE.CubeCamera( 0.1, 1, 512 );
			scene.background = cubeCamera.renderTarget;

			var theta = Math.PI * ( parameters.inclination - 0.5 );
			var phi = 2 * Math.PI * ( parameters.azimuth - 0.5 );

			light_sun.position.x = parameters.distance * Math.cos( phi );
			light_sun.position.y = parameters.distance * Math.sin( phi ) * Math.sin( theta );
			light_sun.position.z = parameters.distance * Math.sin( phi ) * Math.cos( theta );

			sky.material.uniforms['sunPosition'].value = light_sun.position.copy( light_sun.position );
			water.material.uniforms['sunDirection'].value.copy( light_sun.position ).normalize();

			cubeCamera.update( renderer, sky );

			// ==========================
			// 초기화 부분 끝
			// ========================== 

			var framesPerSecond=30;

			// 에니메이션 효과를 자동으로 주기 위한 보조 기능입니다.
			var animate = function () {
				// 프레임 처리
				setTimeout(function() {
					 requestAnimationFrame(animate); 
				}, 1000 / framesPerSecond);

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

				// 랜더링을 수행합니다.
				renderer.render( scene, camera );
			};

			// animate()함수를 최초에 한번은 수행해주어야 합니다.
			animate();		
		</script>
	</body>
</html>