본문 바로가기
자바스크립트와 캔버스

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

by Cray Fall 2019. 7. 13.

three.js 버전 변경으로 기존 소스가 실행이 안되어 수정하였으며, 수정 버전 소스를 맨 마지막에 수록하였습니다.

http://dreamplan7.cafe24.com/canvas/three009.php

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

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

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

모인 물을 바다라 부르시니

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

- 성서 창세기 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>

 

수고하셨습니다!

three.js  버전 변경으로 위 소스는 매우 느리게 작동되며, 속도 빠른 수정 버전을 아래에 기록합니다.

<html>
	<head>
		<title>3차원 캔바스 예제 1</title>
		<style>
			body { margin: 0; }
			canvas { width: 100%; height: 100% }
		</style>
	</head>
	<body>
		<script src="http://dreamplan7.cafe24.com/canvas2/js/three.js"></script>
		<script src="http://dreamplan7.cafe24.com/canvas2/js/OrbitControls.js"></script>
		<script src="http://dreamplan7.cafe24.com/canvas2/js/ColladaLoader.js"></script>		
		<script src="http://dreamplan7.cafe24.com/canvas2/js/Sky.js"></script>		
		<script src="http://dreamplan7.cafe24.com/canvas2/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.shadowMap.enabled = true;
			renderer.shadowMap.type = THREE.PCFShadowMap;		// <-- 속도가 빠르다

			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;
					var i;
					for(i=0;i<model.children.length;++i)
						model.children[i].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].material = new THREE.MeshStandardMaterial({map: texture});
							land.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 );
			var shadowBlur=10;
			light_sun.castShadow=true;
			light_sun.shadow.camera.left=-shadowBlur;
			light_sun.shadow.camera.right=shadowBlur;
			light_sun.shadow.camera.top=shadowBlur;
			light_sun.shadow.camera.bottom=-shadowBlur;

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

			var 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>

다음강좌입니다 / https://itadventure.tistory.com/59

 

3차원 웹, 소스의 체계적 정리, 22번째 시간

지난번 시간까지는 3차원 웹을 구현하는 방법에 대해서 다뤄보았었는데요. 소스가 단순할 때는 느끼지 못했지만 소스가 점점 길어지다보니까 불편한 점이 하나둘 생겨나게 됩니다. ​ 소스가 길어질수록 스크롤의..

itadventure.tistory.com