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

3차원 웹, 빛과 그림자, 캔버스와 함께하는 자바스크립트, 18번째 시간

"빛이 있는 곳에는 어두움도 있다."

천국을 제외한 이 세상은 아직도 빛과 어두움이 공존한다고 생각됩니다 :)

거꾸로 생각하자면 어두움이 있기에 빛이 더욱 밝게 보이기도 하지요.

성서에서도 예수님께서는 빛으로 어두운 세상에 오셨다고 말씀하십니다.

성서 요한복음 1장에서는 아래와 같이 말씀하십니다.

참빛 곧 세상에 와서 각 사람에게 비취는 빛이 있었나니

그가 세상에 계셨으며 세상은 그로 말미암아 지은바 되었으되 세상이 그를 알지 못하였고

자기 땅에 오매 자기 백성이 영접지 아니하였으나

영접하는 자 곧 그 이름을 믿는 자들에게는 하나님의 자녀가 되는 권세를 주셨으니

이는 혈통으로나 육정으로나 사람의 뜻으로 나지 아니하고 오직 하나님께로서 난 자들이니라

성서에 나오는 이 참빛은 바로 예수님이십니다.

이 후 나오는 문맥상 분명할 수 밖에 없습니다.

예수님의 이름을 믿고 영접하는 자, 곧 주인으로 인정하고 모셔들이는 자들을

"하나님의 자녀"로 인정해 주시는 약속을 성경을 통해 해주셨습니다.

하나님의 아들, 하나님의 딸이 된다니 이 얼마나 엄청난 약속이 아닐까요?

만일 마음에 감동이 일어나신다면, 성경을 읽어보시길 권해 드립니다.

성경은 사람이 쓰긴 했지만 결국 그 지은이는 창조의 신 하나님이십니다.

만일 이 글을 읽는 여러분이 하나님의 선택받은 아들, 딸들이라면 분명 말씀을 통해

믿음을 가지게 될 것이라고 확신하니까요.

크레이는 강요하지 않습니다. 다만, 천국갈 수 있는 축복을 강권할 뿐입니다 :)

 


 

자, 본론으로 들어가겠습니다.

3차원웹에서 구현하는 3D월드(?) 캔버스 세상에도 어두움이 있었지요.

거기에 빛이 비추니 상자들이 비로서 모습을 비추게 되는 일이 일어나지 않았습니까?

그런데 여태까지 현실세계에 비교하지만 부자연스러운 부분이 있었는데요.

우선 상자가 허공에 떠 있다는 것이었습니다!

사실 3차원 웹 캔버스의 세계에는 중력이 없습니다.

그러다 보니 상자가 바닥으로 떨어지는 일도 없습니다.

그래도 웬지 떨어지면 불안하니까 허공에 바닥을 깔아 보도록 하겠습니다 :)

지난 소스에 이어서, 가로, 세로 폭이 무려 100이나 되는 크기의 상자를 하나 만들어볼텐데요.

아래와 같이 생긴 소스 밑 부분에,

			// 큐브3
			var mesh3;
			loader.load(
				'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EB%AC%B4%ED%95%9C%EC%BB%A4%ED%94%BC.jpg', 
				function ( texture ) {
					mesh3 = new THREE.Mesh(
						new THREE.BoxGeometry(3, 3, 3), 
						new THREE.MeshStandardMaterial({map: texture})
					);
					mesh3.position.set(-5, 0, 0);
					mesh3.castShadow=true;
					scene.add(mesh3);
				}
			);
        :

이렇게 된 소스를 붙여넣어볼까요?

가로 100미터, 높이 0.1미터, 세로 10미터 크기입니다.

// 바닥
var floor;
floor = new THREE.Mesh(
	new THREE.BoxGeometry(100, 0.1, 100), 
	new THREE.MeshStandardMaterial({color: 0x808080})
);
scene.add(floor);

실행해보니 어라?

상자가 바닥에 묻혀버렸습니다.

그야 당연합니다. 물체의 중심점이 세로 기준으로 동일하니까 바닥도 동일한 높이에 위치해 있는 것입니다.

상자를 3개 다 올리면 번거로우니까 바닥을 조금 아래로 내려볼까요?

이어서 아래 소스를 넣어주면, 좀 위치가 안정적으로 됩니다.

floor.position.set(0, -1.5, 0);

 

그런데 한가지 이상한 부분이 있습니다.

상자들이 그림자가 없다는 점이지요 !

왜 그럴까요?

사실 3차원 컴퓨터 그래픽에서 그림자를 계산하는 건 매우 많은 수학적 연산이 들어가기 때문에 속도가 느려집니다.

그러다 보니 필요할 때만 그림자 옵션을 넣어서 표시하도록 한 것으로 보입니다.

자 먼저 생각해볼까요?

그림자는 물체가 다른 물체가 받아야 할 빛을 가리는 것입니다.

그러나 보니 하나의 물체는 여러 물체에 그림자를 드리울 수 있습니다.

하지만 캔버스의 세계는 수학적 연산을 최소화 하기 위해 그림자를 비추는 물체와

비춰지는 물체의 속성을 하나 하나 정할 수가 있습니다.

어떤 물체는 그림자를 비추지만 어떤 물체는 비추지 않고, 어떤 물체를 비춰지는 그림자의 영역을 받기는 하되 정작 자신은 다른 물체에 그림자를 드리우지는 않는 것이지요.

아래 그림은 이상한 장면이긴 하지만 맨 오른쪽 물체처럼

그림자가 없는 물체가 존재하게 할 수도 있는 것입니다.

재미있는 것은 모든 물체는 그림자가 없는게 기본값이라는 것이지요 :)

그림자를 표현하기 위해선 어떻게 해야 할까요?

4박자가 맞아야 합니다.

첫번째는 렌더러에 기본적으로 그림자를 사용하도록 세팅해야 합니다.

렌더러에 그림자 관련 세팅을 안하면 어떤 그림자도 생겨나지 않습니다.

지난 소스에서 아래와 같이 렌더러 세팅하는 부분 아래에

// 렌더러 정의 및 크기 지정, 문서에 추가하기
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.PCFSoftShadowMap;
renderer.gammaInput = true;
renderer.gammaOutput = true;

여기서 핵심은 renderer.shadowMapEnabled = true; 이 부분입니다.

이 옵션으로 그림자를 지원하도록 바뀌는 것이며

아래 명령어는 좀 더 부드러운 그림자가 되도록 하는 것입니다.

renderer.shadowMap.type = THREE.PCFSoftShadowMap;

그리고 다음 2가지는 화면 전체를 부드럽게 해주는 2가지 옵션으로 겸사해서 작동해주는 것이지요.

renderer.gammaInput = true;

renderer.gammaOutput = true;

2번째로는 빛 자체에 그림자를 적용하도록 옵션을 주는 것입니다.

"렌더러에서 주면 되었지 뭘 또 적용하는건가요?"

그림자도 그림자 나름대로 여러가지 스타일이 있습니다.

경계선이 진한 그림자, 옅은 그림자, 뿌연 그림자 등으로 말이지요 :)

이 기능은 빛에 따라 좌우됩니다.

그래서 빛마다 따로 옵션을 줄 수가 있는 것이지요 :)

경우에 따라서는 빛이 방향에 따라 2단계로 그림자를 줄 수도 있는 부분입니다.

빛에 우선 기본 그림자 속성을 주어 볼까요?

아래 빛을 만드는 소스 아랫 부분에

var light_sun = new THREE.DirectionalLight ( 0x808080, 5.0 );
light_sun.position.set( 200, 200, 300 );
scene.add( light_sun );
     :

다음과 같은 소스를 추가합니다.

light_sun.castShadow=true;
shadowBlur=15;
light_sun.shadowCameraLeft=-shadowBlur;
light_sun.shadowCameraRight=shadowBlur;
light_sun.shadowCameraTop=shadowBlur;
light_sun.shadowCameraBottom=-shadowBlur;

castShadow 는 물체에 그림자를 드리우라는 태양광 빛의 설정입니다.

light_sun.castShadow=true;

그렇다면 shadowCameraLeft, Right, Top, Bottom 은 무엇일까요?

이는 그림자의 blur, 즉 흐림 효과입니다.

최소한 10은 주어야 정상적으로 보이는데요.

이 부분을 설정하지 않으면 아래화면처럼 그림자가 이상하게 보입니다.

사실 이 부분 때문에 크레이도 2시간은 헤멘것 같습니다 :)

외국 자료 구글링해서 간신히 정답을 얻었지요 ㅎ..

하여간 아래 소스는 15정도의 그림자 흐림 효과를 내는데 수치를 키울수록 흐림효과가 더 번져 보입니다.

shadowBlur=15;

light_sun.shadowCameraLeft=-shadowBlur;

light_sun.shadowCameraRight=shadowBlur;

light_sun.shadowCameraTop=shadowBlur;

light_sun.shadowCameraBottom=-shadowBlur;

3번째 박자는 바로 물체의 빛을 가리는 속성입니다.

4번째 박자와 한꺼번에 살펴볼까요?

그림자가 생기려면 빛을 가리는 물체와 빛에 가려진 물체 2가지가 있어야 겠지요?

3번째 박자는 바로 빛을 가리는 물체이고,

4번째 박자는 빛에 가리워진 물체입니다.

모든 물체는 빛을 가리는 물체가 될지 빛에 가리워지는 물체가 될지,

아니면 2가지 모두 해당될지를 설정할 수가 있습니다.

만약 모든 물체에 2가지 속성을 모두 설정하면 현실감은 꽤 나겠지만 상당히 버벅거리며

속도가 느려집니다.

그래서 보통은 바닥 위의 물체는 빛을 가리는 castShadow 속성만 활성화하고,

바닥은 빛에 가리워지는 receiveShadow 속성만 활성화하는 것이 좋습니다.

다음과 같이 수정해볼까요? 3번째 박자입니다 :)

세개의 큐브를 생성한 각각의 소스 중간 중간에

 

// 큐브1
var mesh;
loader.load(
	'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EC%97%B0%EB%82%A0%EB%A6%AC%EA%B8%B0.jpg', 
	function ( texture ) {
		mesh = new THREE.Mesh(
			new THREE.BoxGeometry(3, 3, 3), 
			new THREE.MeshStandardMaterial({map: texture})
		);
		          :
		scene.add(mesh);
	}
);

// 큐브2
var mesh2;
loader.load(
	'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EA%B1%B4%EB%B0%98%ED%9A%A8%EA%B3%BC.jpg', 
	function ( texture ) {
		mesh2 = new THREE.Mesh(
			new THREE.BoxGeometry(3, 3, 3), 
			new THREE.MeshStandardMaterial({map: texture})
		);
               :
		mesh2.position.set(4, 0, 0);
		scene.add(mesh2);
	}
);

// 큐브3
var mesh3;
loader.load(
	'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EB%AC%B4%ED%95%9C%EC%BB%A4%ED%94%BC.jpg', 
function ( texture ) {
		mesh3 = new THREE.Mesh(
			new THREE.BoxGeometry(3, 3, 3), 
			new THREE.MeshStandardMaterial({map: texture})
		);
		mesh3.position.set(-4, 0, 0);
		         :
		scene.add(mesh3);
	}
);

각각 빛을 가리는 속성을 추가합니다.

mesh.castShadow=true;
mesh2.castShadow=true;
mesh3.castShadow=true;

4번째 박자로는 바닥 오브젝트에 그림자를 받아 표시하는 속성을 추가합니다.

바닥 오브젝트를 생성한 이 소스의 아랫 부분에,

// 바닥
var floor;
floor = new THREE.Mesh(
	new THREE.BoxGeometry(100, 0.1, 100), 
	new THREE.MeshStandardMaterial({color: 0x808080})
);
scene.add(floor);
floor.position.set(0, -1.5, 0);
       :

이 소스를 추가해 주시면 됩니다.

floor.receiveShadow=true;

모든 물체에 2가지 속성을 다 정해줄 수 있지만,

그저 속도를 위해서 이렇게 해주는 것임을 알아 주세요 :)

한가지 더 해야 할 일이 있습니다.

그림자를 적용하면 속도가 꽤 느려지는데 컴퓨터 사양이 그리 높지 않다면 화면전환조차 버거울 수가 있습니다.

그래서 동작프레임을 60프레임에서 10프레임으로 확 낮춰 버리면 부담이 훨씬 덜합니다.

이미 우리는 그 기능을 만든 바가 있으므로 아래 변수를 찾아 값을 60에서 10으로 바꿔 주세요.

var framesPerSecond=10;

이것으로 모든 작업을 마쳤습니다.

큰 이슈가 없는 한 아래와 같이 결과물을 보실 수 있을 겁니다 :)

혹시라도 크레이가 빠뜨렸을만한 부분이 있을지 모르니 전체 소스를 게제합니다.

여기까지 열심히 읽어주신 모든 분들께 감사드립니다 :)

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

			// ==========================
			// 초기화 부분 시작 ( 이 부분은 문서에서 한번만 수행되면 됩니다 )
			// ==========================
			// 3차원 세계
			var scene = new THREE.Scene();

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

			// 렌더러 정의 및 크기 지정, 문서에 추가하기
			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.PCFSoftShadowMap; // default THREE.PCFShadowMap
			renderer.gammaInput = true;
			renderer.gammaOutput = true;

			var loader = new THREE.TextureLoader();

			// 큐브1
			var mesh;
			loader.load(
				'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EC%97%B0%EB%82%A0%EB%A6%AC%EA%B8%B0.jpg', 
				function ( texture ) {
					mesh = new THREE.Mesh(
						new THREE.BoxGeometry(3, 3, 3), 
						new THREE.MeshStandardMaterial({map: texture})
					);
					mesh.castShadow=true;
					scene.add(mesh);
				}
			);

			// 큐브2
			var mesh2;
			loader.load(
				'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EA%B1%B4%EB%B0%98%ED%9A%A8%EA%B3%BC.jpg', 
				function ( texture ) {
					mesh2 = new THREE.Mesh(
						new THREE.BoxGeometry(3, 3, 3), 
						new THREE.MeshStandardMaterial({map: texture})
					);
					mesh2.castShadow=true;
					mesh2.position.set(5, 0, 0);
					scene.add(mesh2);
				}
			);

			// 큐브3
			var mesh3;
			loader.load(
				'http://dreamplan7.cafe24.com/SL/img/%EC%B9%B4%EB%93%9C_%EB%AC%B4%ED%95%9C%EC%BB%A4%ED%94%BC.jpg', 
				function ( texture ) {
					mesh3 = new THREE.Mesh(
						new THREE.BoxGeometry(3, 3, 3), 
						new THREE.MeshStandardMaterial({map: texture})
					);
					mesh3.position.set(-5, 0, 0);
					mesh3.castShadow=true;
					scene.add(mesh3);
				}
			);

			// 바닥
			var floor;
			floor = new THREE.Mesh(
				new THREE.BoxGeometry(100, 0.1, 100), 
				new THREE.MeshStandardMaterial({color: 0x808080})
			);
			scene.add(floor);
			floor.position.set(0, -1.5, 0);
			floor.receiveShadow=true;

			// 카메라의 위치 조정
			camera.position.set ( 5, 5, 7 );
			camera.lookAt(0, 0, 0);
			// camera.rotation.set ( -35 * ( Math.PI / 180 ), 35 * ( Math.PI / 180 ), 0 );
	
			// 카메라가 회전하는
			var controls = new THREE.OrbitControls (camera, renderer.domElement);
			controls.update();

			// 전체 조명을 추가합니다.
			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;

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

			var framesPerSecond=10;

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

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

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

이 예제를 볼 수 있는 페이지는 아래에 있습니다.

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

 

3차원 캔바스 예제 1

 

dreamplan7.cafe24.com

 

수고하셨습니다~

 

다음강좌 보러가기 / https://itadventure.tistory.com/55

 

3차원 웹, 그림같은 집을 지어요, 캔버스와 함께하는 자바스크립트, 19번째 시간

미리보기 http://dreamplan7.cafe24.com/canvas/three007.php 3차원 캔바스 예제 1 dreamplan7.cafe24.com 지난 강좌까지는 단순한 상자를 가지고 계속 작업을 했습니다만, 오늘부터는 블렌더에서 직접 모델링한..

itadventure.tistory.com