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

3차원 웹 캔버스, 원터치 버튼 이동! - 24번째 시간

반응형

three.js 버전이 변경되어 소스가 일부 수정되었습니다. ( 2020. 4. 12 )
본문과 일부 일치하지 않는 부분이 있을 수 있으며 아래 URL 에서 작동을 확인해보실 수 있습니다.
http://dreamplan7.cafe24.com/canvas2/three012.html

https://youtu.be/cnARSpNaAV0

웹의 장점은 빠른 찾기 기능입니다.

지금도 언제든지 크레이의 블로그의 메뉴에서 찾고 싶은 메뉴를 클릭하면

언제든지 해당 메뉴 위치로 이동하지요.

3차원 웹에서도 특정지점으로 이동하기 위해 마우스로 낑낑거리며 이동할 수도 있지만,

기왕이면 원클릭으로 한번에 이동하는 기능이 있으면 어떨까요?

이미 영상에서 보신 것처럼 가능합니다.

다만 3차원웹이기 때문에 2차원 웹과는 제공해야 할 정보가 좀 다르다고 할까요?

이를 위해서 카메라의 보는 시점 x, y, z 좌표와 바라볼 좌표 x, y, z 가 필요하지요.

사실 3차원 좌표는 느낌으로 가늠하기란 정말 어려운데요.

먼저 지금 카메라의 3차원 좌표 지점을 늘 화면에 표시하면 어떨까요?

그래서 적당한 위치의 좌표를 메모했다가 사용하면 좋지 않겠습니까? :)

사실 이 글자 부분은 캔바스가 아닙니다.

웹의 div 레이어를 캔바스 위에 조그맣게 띄워놓은 것이지요.

이제 하나씩 과정을 살펴보겠습니다.

먼저 준비작업을 해볼까요?

지난 소스에 이어,

three011.html 파일은 three012.html 파일로,

three011.js 파일은 three012.js 파일로 서버에서 복사해주신 다음

three012.html 파일의 소스를 약간 수정하겠습니다.

아래 부분을

<script src="js/three011.js"></script>

이렇게 바꿔주세요.

<script src="js/three012.js"></script>

그리고 바로 아래에 다음 소스를 추가해줍니다.

<div id=monitor>모니터</div>

하나 더, 스타일을 추가합니다.

아래와 같은 소스를 찾으신 다음

canvas { width: 100%; height: 100% }

그 아랫 부분에 아래 소스를 추가해 주세요.

이 스타일 태그는 id 값이 'monitor'인 항목의 css 를 직접 정의합니다.

#monitor { 
	position: fixed;
	bottom: 0;
    right: 0;
	color:white;
	padding:5px;
	text-align:right;
	font-size:10pt;
}

그리고 three012.html 페이지를 띄워보면,

화면 오른쪽 아랫 부분에 '모니터'라는 한글이 보이실 겁니다.

아직은 아무런 변화가 없지만 앞으로 이 부분에 카메라의 좌표가 나오게 할 겁니다.

참고로, position: fixed 는 화면이 스크롤되든 어떻든간에 항상 고정된 위치에 표시하는 css 속성이고 bottom: 과 함께 여백값을 주면 화면 아래에서 얼마만큼 떨어뜨려 표시할 것인지를 정의하는 속성입니다.

동일한 의미로 right: 속성도 오른쪽에서 얼마만큼 뗄 것인지를 정의하는 속성이고,

color: 는 글자의 색상, padding: 은 div 영역 내에서 글자의 상하좌우 여백을 정의하고,

font-size: 는 글자의 크기를 지정하는 속성이지요.


이제, 이 부분에 카메라의 좌표를 표시하도록 하겠습니다.

three012.js 파일에서 다음 소스를 찾아서,

renderer.render( scene, camera );

그 아랫 부분에 아래 소스를 추가하도록 하겠습니다.

document.getElementById('monitor').innerText = 
	camera.position.x.toFixed(1) + ", " + 
	camera.position.y.toFixed(1) + ", " + 
	camera.position.z.toFixed(1);

그러면 오른쪽 아래 좌표가 이렇게 변경됩니다.

이 좌표가 카메라의 x, y, z 좌표가 됩니다

정말로 그런지 한번 마우스로 화면을 돌려볼까요?

오! 마우스를 돌려보니 좌표가 바뀌었군요!

이 좌표가 바로 현재 카메라의 눈 지점입니다. 이걸 메모해 주시면 이 방향에서 바라보는 좌표를 얻을 수 있습니다.

그렇다면 카메라의 바라보는 지점은 어디일까요?

사실 카메라의 바라보는 지점은 '무한대'이기 때문에 어느 한 지점을 특정 지을 수 없습니다. 바로 1미터 앞이 바라보는 방향일 수도 있고 5미터 또는 10미터, 10.1 미터일 수도 있어서 그렇지요.

굳이 정의하자면, 바라보면서 처음 물건으로 인하여 막히는 지점을 정의할 수도 있지만

이 지점을 찾는 방법은 레이캐스팅(Ray Casting)이라고 해서 차후에 다뤄보도록 하겠습니다.

참고로 기본적인 카메라가 바라보는 지점은 (0, 0, 0)입니다.

이 좌표를 우선 메모해 놓으시기 바랍니다.

자, 이제 원 클릭으로 카메라를 이동하는 기능을 살펴보겠습니다.

먼저 아까 추가했던 아래 소스 부분의 그 아랫 부분에

<div id=monitor>모니터</div>

다음과 같은 레이어를 추가해보겠습니다. 3개의 버튼입니다.

<div id=button1 class=button onclick="moveCam(9, 6, 9.1, 0, 0, 0)">집앞</div>
<div id=button2 class=button onclick="moveCam(85, 116, 160, 3, 16, 63)">산뒤</div>
<div id=button3 class=button onclick="moveCam(-22, 9, -119, 0, 0, 0)">멀리보기</div>

그리고 스타일 태그에서도 다음 소스를 추가해 주세요.

.button
{
	position:fixed;
	width:100px;
	height:20px;
	border:1px solid white;
	cursor:pointer;
	background-color:rgba(101,77,165,0.7);
	padding:5px;
	text-align:center;
	color:white;
}
#button1 { 
	left: 5px;
	top: 5px;
}
#button2 { 
	left: 5px;
	top: 45px;
}
#button3 { 
	left: 5px;
	top: 85px;
}

그러면 이런 요소가 화면에 나타나실 겁니다.

역시 캔버스와는 관련없는 DIV 레이어 요소입니다.

3개의 div 모두에 class=button 이라는 부분이 있는데요.

이 부분은 단지 css 스타일 태그 적용을 위해서 정의한 부분입니다.

중복을 최소화 하기 위해서 그런것이지요.

css 관련 설명은 건너뛰도록 하겠습니다.

2가지만 설명드리자면,

배경색상을 결정하는 속성은 아래와 같은데요.

background-color:rgba(101, 77, 165, 0.7);

앞의 101. 77, 165 는 RGB 색상 코드값이고 0~255범위를 기준으로 합니다.

그리고 마지막의 0.7은 70%의 투명도 정도를 의미합니다.

그래서 뒤로 캔버스가 비쳐보이는 것이지요.

cursor:pointer; 라는 속성은 마우스를 갖다 대면 아래 그림과 같이 손모양으로 바뀌는 기능을 의미합니다.

이제 이 div 요소를 클릭하면, onclick 이벤트에 의해서 moveCam() 함수가 실행이 될텐데요. 사실 아직 눌러봐야 아무 변화가 없습니다. :D :D :D

<div id=button1 class=button onclick="moveCam(9, 6, 9.1, 0, 0, 0)">집앞</div>

카메라 움직임을 담당하는 moveCam()이란 함수를 아직 만든적이 없기 때문이지요.

이 함수는 three.js 내장함수가 아닙니다.

이제, three012.js 파일을 열어,

함수를 추가해볼까요?

기왕 해주는 김에 몇가지 더 수정해봅니다.

맨 윗 줄에 이런 소스가 있을텐데요.

var framesPerSecond=30;

그 아랫 줄에 다음 소스를 추가해 줍니다.

// 태양 초기 위치
parameters.azimuth+=0.2;

controls.enableDamping = true;
controls.dampingFactor = 0.1;

function moveCam(eye_x, eye_y, eye_z, target_x, target_y, target_z)
{
	camera.position.set ( eye_x, eye_y, eye_z );
	controls.target.set( target_x, target_y, target_z );
}

그리고 다음과 같은 소스를 찾아서,

parameters.azimuth+=0.001;

이렇게 바꿔 줍니다.

parameters.azimuth+=0.00001;

밤낮이 너무 빨라서 변화하는 시간을 좀 조정해보았는데요.

태양이 움직이는 속도를 지난 시간의 100분의 1로 낮춘 것이 이 부분입니다.

parameters.azimuth+=0.00001;

그리고 태양의 시작위치를 새벽에서 오전 10시정도로 바꾼 것이 이 부분이고,

parameters.azimuth+=0.2;

다음 소스는 그냥 부가적인 겁니다. 마우스로 화면을 움직일 때 스르륵 부드럽게 움직이게 적용한 것입니다.

controls.enableDamping = true;
controls.dampingFactor = 0.1;

참고로 dampingFactor 0.1 수치를 더 낮추면 마우스를 움직이다가 놓아도 한참동안 더 움직입니다.

이제 다음 소스가 진짜인데요.

카메라의 위치를 바꿔주는 함수입니다.

주어지는 파라미터 eye_x, eye_y, eye_z 는 카메라의 보는 시점의 x, y, z좌표를 의미하고

다음으로 주어지는 파라미터 target_x, target_y, target_z 는 카메라가 바라보는 지점 x, y, z 좌표를 의미합니다, 보통 0, 0, 0을 주면 집이 있는 중앙지점을 향해 바라보게 되지요.

function moveCam(eye_x, eye_y, eye_z, target_x, target_y, target_z)
{
	camera.position.set ( eye_x, eye_y, eye_z );
	controls.target.set( target_x, target_y, target_z );
}

만약 메모하셨던, 좌표지점을 카메라의 눈 지점으로 바라보게 하시려면

아래 소스에서 밑줄친 부분의 좌표값을 바꿔주시면 됩니다 :)

<div id=button1 class=button onclick="moveCam(9, 6, 9.1, 0, 0, 0)">집앞</div>

버튼은 얼마든지 추가하실 수 있으니,

다양한 좌표를 실습해 보시는 건 어떨른지요?

이번 시간에 변경된 소스는 three012.html 파일과 three012.js 파일입니다.

관련 소스를 게제합니다.

<three012.html>

<html>
	<head>
		<title>3차원웹 캔버스</title>
		<style>
			body { margin: 0; }
			canvas { width: 100%; height: 100% }
			#monitor { 
				position: fixed;
				bottom: 0;
		    right: 0;
				color:white;
				padding:5px;
				text-align:right;
				font-size:10pt;
			}
			.button
			{
				position:fixed;
				width:100px;
				height:20px;
				border:1px solid white;
				cursor:pointer;
				background-color:rgba(101,77,165,0.7);
				padding:5px;
				text-align:center;
				color:white;
			}
			#button1 { 
				left: 5px;
				top: 5px;
			}
			#button2 { 
				left: 5px;
				top: 45px;
			}
			#button3 { 
				left: 5px;
				top: 85px;
			}
		</style>
	</head>
	<body>
		<script src="https://threejs.org/build/three.min.js"></script>
		<script src="js/OrbitControls.js"></script>
		<script src="js/ColladaLoader.js"></script>
		<script src="js/Sky.js"></script>		
		<script src="js/Water.js"></script>
		<!-- 이 위쪽은 three.js 라이브러리 -->
		<script src="js/CrayCommon.js"></script>
		<script src="js/LoadCamera.js"></script>
		<script src="js/LoadLight.js"></script>
		<script src="js/LoadSkySea.js"></script>
		<script src="js/LoadModel.js"></script>
		<script src="js/three012.js"></script>
		<div id=monitor>모니터</div>
		<div id=button1 class=button onclick="moveCam(9, 6, 9.1, 0, 0, 0)">집앞</div>
		<div id=button2 class=button onclick="moveCam(85, 116, 160, 3, 16, 63)">산뒤</div>
		<div id=button3 class=button onclick="moveCam(-22, 9, -119, 0, 0, 0)">멀리보기</div>		
	</body>
</html>

<js/three012.js>

var framesPerSecond=30;

// 태양 초기 위치
parameters.azimuth+=0.2;

controls.enableDamping = true;
controls.dampingFactor = 0.1;

function moveCam(eye_x, eye_y, eye_z, target_x, target_y, target_z)
{
	camera.position.set ( eye_x, eye_y, eye_z );
	controls.target.set( target_x, target_y, target_z );
}

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

	if(home_mesh.obj != undefined)
	{
		home_mesh.obj.rotation.set(-90 * PI_PER_180, 0, -45 * PI_PER_180); 
	}

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

	parameters.azimuth+=0.00001;

	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 );
	controls.update();

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

	document.getElementById('monitor').innerText = 
		camera.position.x.toFixed(1) + ", " + 
		camera.position.y.toFixed(1) + ", " + 
		camera.position.z.toFixed(1);
};

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

아울러 본 예제의 실행화면을 직접 볼 수 있는 페이지는 아래와 같습니다.

http://dreamplan7.cafe24.com/canvas2/three012.html

 

3차원웹 캔버스

 

dreamplan7.cafe24.com

수고하셨습니다.

여기까지 시청해주셔서 감사합니다 :)

다음 강좌로 가는 길! / https://itadventure.tistory.com/65

 

3차원 웹, 창공을 누비는 카메라! - 25번째 시간

사람이 새처럼 하늘을 날아다닐 수 있다면 얼마나 좋을까요? 어디든지 순식간에 휙 날아갈 수 있게 말이죠. ​ 위험도 없고 염려없이 하늘을 날 수 있다면 마치 천사와 같은 삶이 아닐지요. 성서에서 말하는 천국..

itadventure.tistory.com

 

반응형
  • Favicon of https://bubleprice.tistory.com BlogIcon 버블프라이스 2019.07.18 07:44 신고

    지난 시간에 이은 3차원 웹 캔버스 코딩 공부를 덕분에 잘하고 갑니다^^ 즐거운 목요일 보내시길 바래요

  • Favicon of https://moldone.tistory.com BlogIcon 팡이원 2019.07.19 07:12 신고

    굉장히 무더운 날씨 입니다
    건강 유의 하시고 좋은 하루 보내세요~
    포스팅 잘 보고 가네요~

  • web developer 2022.02.07 15:41

    안녕하세요 좋은 글 잘 보고있습니다.
    연습용 프로젝트를 만드는 중 궁금증이 있어서 댓글 남깁니다.
    현재는 마우스의 위치와 상관없이 카메라의 방향은 고정인데 예를 들어 마우스를 산에 두고 마우스 휠을 움직일때 산이 중앙이 되어 확대 / 축소를 하고 싶은데 어떻게 해야 할까요??

    • Favicon of https://itadventure.tistory.com BlogIcon CrayFall 2022.02.07 18:35 신고

      moveCam 에서 대상의 좌표를 산의 정상에서 약간 내려온 지점으로 주시면 되는데요. 산의 중심점이 아닌 점에 유의해 주세요. 하나의 좌표 예시를 드립니다.
      moveCam(-90, 70, -110, 0, 40, 100)

      아래 페이지에 마련해놓았습니다. 산쪽보기 버튼을 클릭해 주세요.
      http://dreamplan7.cafe24.com/canvas2/three012_2.html

      방문해 주셔서 감사합니다 :)

  • web developer 2022.02.08 11:16

    안녕하세요 답변 감사드립니다.
    크레이님이 말씀하신대로 하였는데 궁금점이 생겨 질문드립니다.
    moveCam의 파라미터들인 target_x, target_y, target_z은 vector3값을 의미 하는것이 맞을까요?
    그리고 각각의 버튼들을 눌렀을때 오른쪽하단의 좌표의 수치는 맞게 이동하는데 뷰화면이 다른곳을 가리키다가 마우스 휠을 움직이면
    원래대로 돌아오는 오류가 있는데 혹시 어떻게 해야 하는지 알 수 있을까요?

    • Favicon of https://itadventure.tistory.com BlogIcon CrayFall 2022.02.08 23:14 신고

      target_x, target_y, target_z 은 벡터가 맞습니다. 3차원 그래픽에서 3D벡터는 두가지가 많이 사용되는데요. 방향벡터와 점벡터입니다. 방향벡터는 현재 기준위치를 대상으로 특정 방향을 의미하는 벡터이고, 점벡터는 대상이 되는 정점을 의미하지요.
      이 경우에는 대상의 점백터입니다. 카메라가 바라볼 점벡터를 지정해주면 해당 방향으로 시점을 돌아보아 카메라의 방향벡터가 바뀌는 구조입니다.
      마우스휠은 카메라가 이동하는 기능이라 자동 이동중에는 금지시켜야 하는 기능인데 해당 코드가 누락되었습니다. 사실 이 때는 마우스를 통한 카메라 조작이 금지되어야 할 필요가 있습니다.
      조만간 코드 하나 짜서 공유드리겠습니다 :)

  • web developer 2022.02.09 14:48

    좋은 답변 정말 감사합니다. 많은 도움이 되었습니다.
    죄송하지만 한가지 더 여쭈어 보려 합니다.
    커튼, 침대등을 블렌더에서 모델링 후 ColladaLoader를 사용해 가져온 후 기본 디자인 이미지와 재질을 표현해주는 이미지를 넣어서
    현실적이게 보이게 하기위해

    const curtain = new THREE.MeshStandardMaterial({
    map: map,
    normalMap: normalMap ,
    });

    이런식의 코드를 작성하였는데 현실감이 떨어지는데 혹시 다른 방법이 있을까요?

    • Favicon of https://itadventure.tistory.com BlogIcon CrayFall 2022.02.09 19:25 신고

      웝엔진은 재질효과가 한계가 있을 겁니다. 재질이 멋지게 보이려면 HDRI 배경이 물체 표면에 반사되어야 하는데 웹에서는 시도해본적은 없습니다. 다른 구상안으로 블렌더 2.79 버전에서는 사이클 렌더를 이용해 물체표먼을 베이크(굽는) 기법이 있었는데요. 해당방법으로 금반지 효과를 낸 적이 있었지요. 2.8부터 확 바뀌어서 가능한지는 확인 못 해봤습니다. 버전이 많이 달라 보시기에는 불편하실 텐데요. 우선 이 url을 참조하시면 어떨까 합니다. : 블렌더 따라잡기 7일차. UV 맵으로 베이크하기! 빵굽기? - https://itadventure.tistory.com/m/41

  • web developer 2022.02.10 17:28

    귀찮으실텐데 답변 정말 감사합니다ㅠ
    한가지 더 궁금한점이 있습니다.

    var loader = new THREE.TextureLoader();
    const map = loader.load(
    // "../examples/test/test.png",
    "ggg/base3.jpg",
    // "img/9asd.png",
    // "images/occtest.png",
    texture1 => {
    texture1.repeat.x = 2;
    texture1.repeat.y = 2;
    texture1.wrapS = THREE.RepeatWrapping;
    texture1.wrapT = THREE.RepeatWrapping;
    texture1.magFilter = THREE.LinearFilter;
    texture1.minFilter = THREE.LinearMipmapLinearFilter;
    }
    );

    const normalMap = loader.load(
    // "images/llll.jpg",
    "ggg/normal4.jpg",
    texture2 => {
    texture2.repeat.x = 1;
    texture2.repeat.y = 1;
    texture2.wrapS = THREE.RepeatWrapping;
    texture2.wrapT = THREE.RepeatWrapping;
    texture2.magFilter = THREE.LinearFilter;
    texture2.minFilter = THREE.LinearMipmapLinearFilter;
    }
    );

    const curtain = new THREE.MeshStandardMaterial({
    map: map,
    normalMap: normalMap,
    });

    이런식의 코드를 작성하여 뷰를 보면 map 의 이미지는 2번씩 반복, normalMap의 이미지는 1번만 반복 되어야하는데
    normalMap의 repeat 값은 어떤 값이 와도 반영되지 않고 map의 repeat값으로만 반영이되는데 이게 정상인걸까요?

    그리고 저렇게하고난뒤 화면을 보면 다른 부분들은 나름 퀄리티 있게 잘 표현이 되는데 이미지의 하얀 부분만 마치 반사되는것처럼
    반짝거려서 light쪽을 만져야 할 것 같은데 감이 잘 오지 않습니다ㅠ
    혹시 도움을 받을 수 있을까요?

    • Favicon of https://itadventure.tistory.com BlogIcon CrayFall 2022.02.10 23:18 신고

      조만간 관련 부분을 알아봐야겠군요 :) 광택은 그리 쉽게 해결되는 부분은 아니지만 방법은 있는 것 같습니다.
      아래 사이트에 방문하시면 세계지도중에서 빛이 비추는 부분과 비추지 않는 부분을 명확하게 확인하실 수 있는데요.
      역시 three.js 로 제작되었습니다. SpecularMap 이란걸 사용하였는데 빛이 비추는 강도를 텍스쳐로 사용하는 기술입니다.
      관련 부분 확인해보시면 힌트를 얻으실 수 있을 것 같습니다.
      https://sbcode.net/threejs/specularmap/

      repeat 관련해서 찾아보았는데 다음 URL에 외국분이 답변을 단 게 있습니다.
      크레이도 해보지는 않았는데 힌트가 되지 않을까 생각됩니다 :) 화이팅입니다~

      https://stackoverflow.com/questions/14114030/how-to-write-right-repeat-texture-with-three-js

      var loader = new THREE.TextureLoader();

      var texture = loader.load( 'myTexture.jpg', function ( texture ) {

      texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
      texture.offset.set( 0, 0 );
      texture.repeat.set( 2, 2 );

      } );

      var material = new THREE.MeshPhongMaterial( {

      color: 0xffffff,
      specular:0x111111,
      shininess: 10,
      map: texture,
      . . .

      } );

  • web developer 2022.02.11 10:19

    답변 정말 감사합니다. 많은 도움이 되었습니다.

    마지막으로 한가지만 더 여쭤보고 싶습니다.
    화면상에서의 현재 내 마우스 위치의 좌표값을 얻고 싶은데 어떻게 해야 할까요?

    • Favicon of https://itadventure.tistory.com BlogIcon CrayFall 2022.02.11 23:57 신고

      마우스 좌표는 2가지 개념이 있습니다.
      짧은 메모로는 좀 이해가 어려울 수 있어 관련 게시글을 작성하였으니 참조해 주세요 :)
      https://itadventure.tistory.com/488