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

3차원 웹, 하늘과 바다를 만들어 봐, 캔버스와 함께하는 자바스크립트, 20번째 시간

https://youtu.be/ZNrmBoMf0YE

 

하나님이 이르시되

"물 가운데에 궁창이 있어 물과 물로 나뉘라"

하시고 하나님이 궁창을 만드사

궁창 아래의 물과 궁창 위의 물로 나뉘게 하시니

그대로 되니라

하나님이 궁창을 하늘이라 부르시니라

저녁이 되고 아침이 되니 이는 둘째 날이니라

 

성서의 창세기 1장, 지구를 창조하신 하나님께서 두번째 날에 행하신 일로 기록됩니다.

"물 가운데 궁창이 있어 물과 물로 나뉘라!"

궁창(穹蒼)은 '푸른 하늘'이라는 한자어입니다.

영어 성경 킹제임스 버전에서도, 다음과 같이 말씀하시지요.

Let there be / a firmament / in the midst / of the waters,

거기에 있게 되어라 / 하늘이 / 한 가운데에 / 물들의,

and let it divide / the waters / from the waters.

그리고 분리되어라 / 물들이 / 물들로부터.

물들로 가득한 지구땅에 바깥 막에 있는 물들이 부풀어오르듯 하늘로 솟구치며,

그 가운데 창공이 생겨난 것이지요. 매우 아름다운 광경이지요.

하늘 위에 물이 떠 있을 수 있을까요? 물은 공기보다 밀도가 높기 때문에 가라앉을 수 밖에 없는 것은 인간이 현대 과학으로 증명할 수 있는 한계이지만 하나님은 어떠한 분이신가요? 전능하신 분 앞에서 인간의 과학은 아주 사소한 것만을 볼 수 있을 뿐입니다 :)

그러니 하늘과 땅이 하나님을 찬양하는 것은 아주 당연합니다.

만일 감동이 느껴지신다면 찬양 곡 한곡 들으시면서 이번 강좌 들어보시는 것은

어떨까요? :)

https://youtu.be/hyCXdZfnwxk

지난 시간에는 3차원 공간 안에 집과 바닥을 창조해보았지요?

근데 바닥이 좀 밋밋하니, 돌바닥으로 바꿔보겠습니다.

회색 바닥을 생성하는 아래 코드를 지우고,

floor = new THREE.Mesh(
	new THREE.BoxGeometry(100, 0.1, 100), 
	new THREE.MeshStandardMaterial({color: 0x808080})
);

이렇게 바꿔볼까요? 참고로 바닥의 크기도 좀 줄었습니다.

floor = new THREE.Mesh(
	new THREE.BoxGeometry(10, 0.1, 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;
	}
);

바닥이 벽돌 무늬가 되었습니다.

여기에 적용된 무늬인 벽돌 텍스쳐는 이렇게 생긴 이미지인데, 무료 이미지 사이트 픽사베이에서 다운받아 상하좌우 반복되도록 편집하였습니다.

http://dreamplan7.cafe24.com/canvas/img/floor1.jpg

먼저 이 부분은 표면이 아무것도 정의되지 않은 얇은 상자를 하나 만드는 것입니다.

floor = new THREE.Mesh(
	new THREE.BoxGeometry(10, 0.1, 10)
);

그리고 나서, 이미지 파일을 불러와서 3차원 물체 표면에 텍스쳐로 적용하는 것이지요.

지난번에 정육면체에 무늬 입힌 것과 같은 구조입니다.

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;
	}
);

그런데 새로운 부분이 하나 생겼군요?

바로 repeat 와 RepeatWrapping 이라는 부분입니다.

floor.material.map.repeat.x=3;
floor.material.map.repeat.y=3;
floor.material.map.wrapS=THREE.RepeatWrapping;
floor.material.map.wrapT=THREE.RepeatWrapping;

이 부분은 벽돌텍스쳐가 물체의 표면에 입혀질 때 반복할 횟수를 지정하는 부분입니다.

반복 횟수를 가로, 세로 3번씩 해주었기 때문에, 아래와 같이 촘촘하게 벽돌이 구성되었습니다.

이제 바닥도 좀 뽀대(?) 나게 되었으니 밋밋한 하늘을 꾸며보도록 하겠습니다.

원래는 스카이박스라고 해서 이미지로 하늘을 꾸미는게 있긴 한데,

공모양의 스카이 박스는 360 VR 카메라가 있어야 촬영이라도 할 수 있고,

큐브 모양의 스카이 박스라고 해서 이미지를 6개 준비하는 방식이 있는데

만들어서 적용해본 결과 여간 맞추기 힘들더라구요 :)

그래서 검색해본 결과 더욱 멋진 효과를 발견하여 적용해보았습니다.

실제 하늘과 같은 수준의 그래픽 효과인데요.

일부 구형 PC에서는 혹시 작동안될까 약간 염려스럽기도 합니다.

이를 위해 three.js 에서 제공하는 하늘 셰이더 스크립트가 사용됩니다.

먼저 스크립트를 설정하는 이 부분 소스를 찾으신 다음에,

<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="http://dreamplan7.cafe24.com/canvas/js/Sky.js"></script>

그리고 태양빛을 생성하는 스크립트 아랫 부분에,

light_sun.shadowCameraTop=shadowBlur;
light_sun.shadowCameraBottom=-shadowBlur;
        :

아래 소스를 추가해 줍니다.

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 );

cubeCamera.update( renderer, sky );

소스가 꽤 길지요? 그런만큼 보람은 큽니다. 결과를 볼까요?

뭔가 대기의 분위기가 바뀌었지요?

약간 카메라를 돌려보면 와우! 아주 멋집니다.

아래 부분은 하늘 오브젝트를 만드는 부분입니다.

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;

시험삼아 turbidity 값을 바꿔보니

sky.material.uniforms['turbidity'].value=1;

이렇게 되더군요 :) 뭔가 번짐효과 처럼 느껴집니다.

다음 파라미터 정의 부분에서도 흥미로운 부분은 azimuth 입니다.

var parameters = {

distance: 400,

inclination: 0.1,

azimuth: 0.05

};

태양의 위치가 바뀌거든요. 해가 뜨고 진다고 보면 됩니다.

값을 변경함에 따라 아주 다양한 연출이 좌우됩니다.

azimuth: 0.0

azimuth: 0.02

azimuth: 0.1

azimuth: 0.25 - 그림자 위치를 보면 완전 정오입니다. 하늘이 푸르르네요.

그 이하 나머지 소스는 아직 크레이도 100% 이해 못했기 때문에 넘어가도록 하겠습니다.

이제 바다를 만들어 보도록 할까요?

바다를 만들기 위해서 먼저 하늘처럼 js 파일을 포함시켜야 합니다.

아까 추가한 하늘 스크립트 불러오는 아랫 부분에,

<script src="http://dreamplan7.cafe24.com/canvas/js/Sky.js"></script>
         :

바다 스크립트를 추가해보겠습니다.

<script src="http://dreamplan7.cafe24.com/canvas/js/Water.js"></script>

그리고 바다를 만들어보겠습니다.

아까처럼 태양광을 생성하는 아랫부분에,

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 );

멋진 물이 생겨났습니다만, 물이 미동하지 않습니다.,

처음 영상에서는 분명 물이 움직였는데 말이죠.

물을 움직이려면 애니메이션에서 물 텍스쳐의 time 값을 지속적으로 변경시켜주어야 합니다. 아래와 같은 소스 아랫 부분에,

var animate = function () {
	// 프레임 처리
	setTimeout(function() {
		 requestAnimationFrame(animate); 
	}, 1000 / framesPerSecond);
           :

아래 소스를 추가해 주세요.

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

물이 막 흘러갑니다. 멋지지요?

이 외에도 몇가지 더 손을 본 부분이 있지만 사소한 부분이라 간단히 정리하겠습니다.

전체 소스 따로 게제하니 그냥 참고만 해 주세요.

우선 자그마한 큐브섬의 느낌을 주기 위해, 큐브 크기를 작게 줄이고,

물위에 떠 있도록 위치를 바닥과 집의 위치를 조정하였습니다.

var floor;
floor = new THREE.Mesh(
	new THREE.BoxGeometry(10, 10, 10)
);
          :
floor.position.set(0, -3, 0);

그리고 카메라가 물 아래쪽으로 가면 물 속에서 보는 느낌이 안 들기 때문에

카메라의 회전 반경을 제한하였지요.

// 카메라가 회전하는
var controls = new THREE.OrbitControls (camera, renderer.domElement);
controls.enablePan = false;
controls.minPolarAngle = Math.PI / -2;
controls.maxPolarAngle = Math.PI / 2.1;

반영된 전체 소스는 아래와 같습니다.

여기까지 읽어 주셔서 감사합니다 :)

<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, 0.1, 50000 );

			// 렌더러 정의 및 크기 지정, 문서에 추가하기
			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 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 );

      });

			// 바닥
			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( '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>

실제 예제 페이지

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

 

3차원 캔바스 예제 1

 

dreamplan7.cafe24.com