diff --git a/index.html b/index.html
index bc519e5a955d792057e4e5dde4f8585e15a47d67..b5db9c7fa9cf1cb862e43bb14b7813c5d77f4d5a 100644
--- a/index.html
+++ b/index.html
@@ -1,25 +1,2503 @@
-<!DOCTYPE html>
 <html>
 <head>
 	<meta charset="utf-8">
-	<title>CG Course Final Project</title>
+	<title>Light Shaft Tutorial</title>
 	<style>
 		body {
-			margin: 0;
-			overflow: hidden;
+			margin: 50px;
+			color: black;
+		}
+
+		code {
+			padding: 0.2em;
+			font: 0.85em consolas;
+			background-color: #f1f1f1;
+		}
+
+		table {
+			color: #030303;
+			font: 0.95em consolas;
+			position: relative;
+			background-color: #f1f1f1;
+		}
+
+		td {
+			padding: 6px;
+		}
+
+		.result {
+			width: 50%;
 		}
 	</style>
 </head>
 <body>
-	<div id="container"></div>
+	<h1>CG Course Final Project: Light Shaft Tutorial</h1>
 
-	<script type="importmap">
-		{
-			"imports": {
-				"three": "../build/three.module.js"
-			}
-		}
-	</script>
-	<script type="module" src="script.js"></script>
+	<hr /> <!-- 파트 분리선: 여기서 소개 & 목차 시작 -->
+
+	<div id="intro">
+		<h2>소개</h2>
+		<ul>
+			<li><b>학과</b>: 소프트웨어학과</li>
+			<li><b>학번</b>: 201820749</li>
+			<li><b>이름</b>: 한수현</li>
+		</ul>
+
+		<h2>목차</h2>
+		<p>
+			<b>1장.</b> 주제 및 목표<br />
+			<b>2장.</b> 기본 장면 렌더링<br />
+			<b>3장.</b> 카메라와 GUI 조작<br />
+			<b>4장.</b> 렌더 타겟<br />
+			<b>5장.</b> 가중치 추출<br />
+			<b>6장.</b> 라이트 셰프트<br />
+			<b>7장.</b> 꾸미기<br />
+			<b>8장.</b> 소스 & 레퍼런스
+		</p>
+
+		<h2>안내</h2>
+		<p>
+			본 프로그램은 <b>Visual Studio 2022</b>의 <b>node.js 프로젝트</b>에서 개발되었습니다.<br />
+			실행은 반드시 <b>Three.js NPM이 설치된 서버 환경</b>에서 해주세요.<br />
+			로컬 환경에서 실행하면 브라우저 보안상의 이유로 로컬 파일을 불러올 수 없습니다(<em>텍스처, 모델, 스크립트 등</em>).
+		</p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 1장 시작 -->
+
+	<div id="chapter_1">
+		<h2>1장: 주제 및 목표</h2>
+
+		<h3>1.1 주제</h3>
+		<p>
+			제가 선정한 주제는 <b>라이트 셰프트(Light Shaft) 효과</b>로, 오브젝트 사이로 스며들어오는 빛 줄기의 구현입니다.<br />
+			기존 Three.js 예제에서 수정한 결과물은 아니지만 비슷한 예제로 <b>Godrays</b>가 있습니다.<br />
+			다만 <b>Godrays</b> 예제의 경우 셰이더 패스가 많고 과정이 복잡하여 필요없는 부분(<em><u>FakeSun 패스, Combine 패스</u></em>)을 빼고, 셰이더 코드도 더 쉽도록 새로 만들었습니다.<br />
+			즉, 백지 스크립트에서 제작되었으며 튜토리얼 또한 백지 스크립트에서 시작합니다.<br />
+		</p>
+
+		<h3>1.2 목표</h3>
+		<p>
+			<b>1.</b> 기본적인 렌더링 장면 구현하기<br />
+			<b>2.</b> OrbitControls를 사용한 카메라의 움직임 구현하기<br />
+			<b>3.</b> 렌더 타겟과 영상 처리에 대해 이해하기<br />
+			<b>4.</b> EffectComposer와 ShaderPass 사용법 이해하기<br />
+			<b>5.</b> GLSL 프로그래밍 과정 및 문법 이해하기<br />
+			<b>6.</b> 깊이 텍스처로부터 가중치 추출하기<br />
+			<b>7.</b> 라이트 셰프트 셰이더 만들기
+		</p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 2장 시작 -->
+
+	<div id="chapter_2">
+		<h2>2장: 기본 장면 렌더링</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_2.png" alt="Chapter 2 result" title="Chapter 2 result"></p>
+
+		<details>
+			<summary><b>2장 튜토리얼 (펼치기/접기)</b></summary>
+			<p>
+				2장에서는 <b>베이스 프로그램</b>을 만들건데요.<br />
+				<code>index.html</code>의 경우 내용이 적고 자바스크립트가 아니므로 따로 해설을 하진 않고 첨부만 하겠습니다.<br />
+				추가로 저처럼 <code>node.js</code>로 서버를 여시는 분들을 위해 <code>server.js</code> 파일도 함께 제공하겠습니다.
+			</p>
+
+			<h3>2.1 index.html & server.js</h3>
+			<p>
+				<mark><code>index.html</code> 코드는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/index.html" title="index.html"><b>여기</b></a>에 있습니다.</mark><br />
+				<mark><code>server.js</code> 코드는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/server.js" title="server.js"><b>여기</b></a>에 있습니다.</mark>
+			</p>
+
+			<p>
+				자, 이제 본격적으로 <code>script.js</code> 파일을 만들어 봅시다.<br />
+				기본적인 장면만 구성해볼 건데요.<br />
+				대충 바닥 있고 나무 몇 개 있고 라이트 좀 있는 그런 장면을 말하는 겁니다.<br />
+				대부분이 수업에 나온 내용이므로 가볍게 따라와주시면 되겠습니다.<br />
+				빈 <code>script.js</code> 파일을 준비해주세요.
+			</p>
+
+			<h3>2.2 모듈</h3>
+			<p>
+				가장 먼저 해야할 것은 <b>모듈</b> 불러오기입니다.<br />
+			</p>
+
+			<!-- 2장 모듈 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						import * as THREE from 'three';							<br />
+						import { Vector3, Color } from 'three';					<br />
+						import Stats from './jsm/libs/stats.module.js';			<br />
+						import { OBJLoader } from './jsm/loaders/OBJLoader.js';
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 맨 위에 추가해주세요.<br />
+				첫째 줄은 Three.js의 모든 <b>코어 라이브러리</b>를 불러옵니다.<br />
+				둘째 줄은 프로그램에서 사용할 <b>클래스</b>들입니다.<br />
+				각각 3차원 벡터와 색상에 해당됩니다.<br />셋째 줄은 <b>스탯 모듈</b>인데요.<br />
+				수업에서 배우지는 않았지만 이걸 사용하면 결과물의 왼쪽 상단에 프레임, 루프 소요 시간, 메모리 등의 정보를 보여줍니다.<br />
+				필수는 아니지만 만들고 있는 프로그램의 사양을 파악할 수 있으므로 넣어봅시다.<br />
+				넷째 줄은 <b>오브젝트 로더</b>입니다.<br />
+				이걸 이용해서 기본 모델 중 하나인 <code>tree.obj</code> 파일을 불러올 계획입니다.
+			</p>
+
+			<h3>2.3 상수</h3>
+			<p>
+				다음으로 할 일은 프로그램에서 사용할 <b>상수</b>의 정의입니다.<br />
+				<b>C언어</b>로 치자면 <code>#define</code> 같은 것이죠.<br />
+				예상 외로 많지만 일단 따라 쳐보도록 합시다.
+			</p>
+
+			<!-- 2장 상수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const constants = {																									<br />
+						&nbsp; &nbsp; defaultCameraPosition: new Vector3(0, 20, 200), // 카메라의 기본 위치									<br />
+						&nbsp; &nbsp; defaultCameraLookAt: new Vector3(0, 70, 0), // 카메라가 바라보는 기본 위치								<br />
+						&nbsp; &nbsp; defaultFieldOfView: 60, // 카메라의 기본 시야각															<br />
+						&nbsp; &nbsp; defaultBackgroundColor: new Color(0xcccccc), // 렌더러의 기본 바탕색										<br />
+						&nbsp; &nbsp; defaultAmbientLightColor: new Color(0x555555), // 앰비언트 라이트의 기본 색								<br />
+						&nbsp; &nbsp; defaultDirectionalLightColor: new Color(0x555555), // 디렉셔널 라이트의 기본 색							<br />
+						&nbsp; &nbsp; defaultSunObjectGeometry: new THREE.SphereGeometry(1, 24, 12), // 태양 오브젝트의 기본 지오메트리			<br />
+						&nbsp; &nbsp; defaultSunObjectMaterial: new THREE.MeshBasicMaterial({ color: 0xffffff }), // 태양 오브젝트의 기본 재질	<br />
+						&nbsp; &nbsp; maxSunDistance: 600, // 태양 오브젝트의 최대 거리															<br />
+						&nbsp; &nbsp; treeCount: 10, // 나무의 개수																			<br />
+						&nbsp; &nbsp; treeOffset: 50 // 나무들의 간격																			<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				하나하나 설명하기엔 간단해서 주석으로 대신합니다.<br />
+				기본 장면이지만 대부분의 상수가 기본 값이기 때문에 많이 들어가는군요.
+			</p>
+
+			<h3>2.4 실행 함수</h3>
+			<p>
+				다음은 이제 <b>실행 함수</b>의 차례입니다.
+			</p>
+
+			<!-- 2장 실행 함수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function run() {														<br />
+						&nbsp; &nbsp; const scene = new THREE.Scene();							<br />
+						&nbsp; &nbsp; const stats = new Stats();								<br />
+						&nbsp; &nbsp; const container = document.getElementById('container');	<br />
+						&nbsp; &nbsp; container.appendChild(stats.dom);							<br />
+						&nbsp; &nbsp;															<br />
+						&nbsp; &nbsp; let renderer, camera;										<br />
+						&nbsp; &nbsp; let ambientLight, directionalLight;						<br />
+						&nbsp; &nbsp; let sunObject;											<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				간단하게 이렇게만 추가해봅시다.
+			</p>
+
+			<!-- 2장 실행 함수 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						const scene = new THREE.Scene();						<br />
+						const stats = new Stats();								<br />
+						const container = document.getElementById('container');	<br />
+						container.appendChild(stats.dom);						<br />
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 <b>씬</b>과 <b>스탯</b>, <b>컨테이너</b>를 생성합니다.<br />
+				컨테이너의 경우 <code>index.html</code>의 div로 선언된 영역 <code>container</code>에 해당됩니다.
+			</p>
+
+			<!-- 2장 실행 함수 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						let renderer, camera;				<br />
+						let ambientLight, directionalLight;	<br />
+						let sunObject;						<br />
+					</td>
+				</tr>
+			</table>
+			<p>
+				그리고 <b>렌더러</b>, <b>카메라</b>, <b>앰비언트 라이트</b>, <b>디렉셔널 라이트</b>, <b>태양 오브젝트</b>를 선언해둡니다.<br />
+				지금 당장 생성하지 않을 오브젝트들이므로 <code>let</code>으로 선언합니다.
+			</p>
+
+			<h3>2.5 렌더러</h3>
+			<p>
+				이제 <b>렌더러</b>를 생성할 시간입니다.<br />
+				<code>run()</code> 함수에 그대로 쓰면 지저분해지므로 함수로 따로 빼도록 합시다.<br />
+				하지만 전역이 아닌 <code>run()</code> 함수의 내부에 정의해주세요.
+			</p>
+
+			<!-- 2장 렌더러 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createRenderer() {														<br />
+						&nbsp; &nbsp; renderer = new THREE.WebGLRenderer();								<br />
+						&nbsp; &nbsp; renderer.setSize(window.innerWidth, window.innerHeight);			<br />
+						&nbsp; &nbsp; renderer.setClearColor(constants.defaultBackgroundColor, 1.0);	<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; container.appendChild(renderer.domElement);						<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				렌더러를 생성하고 몇 가지의 설정을 합니다.
+			</p>
+
+			<!-- 2장 렌더러 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						renderer = new THREE.WebGLRenderer();
+					</td>
+				</tr>
+			</table>
+			<p>
+				생성은 <code>THREE.WebGLRenderer()</code> 생성자를 호출하면 됩니다.
+			</p>
+
+			<!-- 2장 렌더러 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						renderer.setSize(window.innerWidth, window.innerHeight); // 영역 크기 설정	<br />
+						renderer.setClearColor(constants.defaultBackgroundColor, 1); // 바탕색 설정
+					</td>
+				</tr>
+			</table>
+			<p>
+				그리고 픽셀 비율, 영역 크기, 바탕색을 설정합니다.<br />
+				영역 크기는 창의 내부 크기로 각각 설정해주면 됩니다.<br />
+				바탕색의 경우 앞서 정의했던 <code>constants</code>의 렌더러 기본 바탕색을 사용해주도록 합시다.<br />
+				뒤의 <code>1</code> 은 알파 값으로서, 바탕색이 투명하면 안 되므로 <code>1</code>로 해줍니다.
+			</p>
+
+			<!-- 2장 렌더러 코드 1-3 -->
+			<table>
+				<tr>
+					<td>
+						container.appendChild(renderer.domElement);
+					</td>
+				</tr>
+			</table>
+			<p>
+				마지막으로 컨테이너에 추가해주면 렌더러는 준비 완료입니다!
+			</p>
+
+			<h3>2.6 카메라</h3>
+			<p>
+				<b>카메라</b>를 생성해봅시다.<br />
+				마찬가지로 함수로 분리합니다.
+			</p>
+
+			<!-- 2장 카메라 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createCamera() {																											<br />
+						&nbsp; &nbsp; camera = new THREE.PerspectiveCamera(constants.defaultFieldOfView, window.innerWidth / window.innerHeight, 1, 1000);	<br />
+						&nbsp; &nbsp; camera.position.copy(constants.defaultCameraPosition);																<br />
+						&nbsp; &nbsp; camera.lookAt(constants.defaultCameraLookAt);																			<br />
+						&nbsp; &nbsp; scene.add(camera);																									<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				렌더러와 마찬가지로 생성 후 값을 좀 수정하고 부모 오브젝트에 추가합니다.
+			</p>
+
+			<!-- 2장 카메라 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						camera = new THREE.PerspectiveCamera(constants.defaultFieldOfView, window.innerWidth / window.innerHeight, 1, 1000);
+					</td>
+				</tr>
+			</table>
+			<p>
+				카메라는 <b>Perspective</b> 카메라로 준비합니다.<br />
+				수업을 열심히 들으셨다면 각각의 인자가 무엇을 뜻하는지 아시겠죠?<br />
+				시야각, 종횡비, near, far입니다.<br />
+				시야각의 경우 역시 <code>constants</code>를 사용합니다.
+			</p>
+
+			<!-- 2장 카메라 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						camera.position.copy(constants.defaultCameraPosition);	<br />
+						camera.lookAt(constants.defaultCameraLookAt);
+					</td>
+				</tr>
+			</table>
+			<p>
+				그리고 카메라 위치와 타겟 위치를 설정합니다.<br />
+				<code>constants</code>의 값을 보시면 아시겠지만 약간 위를 바라보는 각도입니다.
+			</p>
+
+			<!-- 2장 카메라 코드 1-3 -->
+			<table>
+				<tr>
+					<td>
+						scene.add(camera);
+					</td>
+				</tr>
+			</table>
+			<p>
+				마지막으로 씬에 넣어줍니다.
+			</p>
+
+			<h3>2.7 라이트</h3>
+			<b>라이트</b>를 생성해봅시다.
+
+			<!-- 2장 라이트 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createLights() {																				<br />
+						&nbsp; &nbsp; ambientLight = new THREE.AmbientLight(constants.defaultAmbientLightColor);				<br />
+						&nbsp; &nbsp; scene.add(ambientLight);																	<br />
+						&nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; directionalLight = new THREE.DirectionalLight(constants.defaultDirectionalLightColor);	<br />
+						&nbsp; &nbsp; directionalLight.position.set(0, constants.maxSunDistance, -constants.maxSunDistance);	<br />
+						&nbsp; &nbsp; scene.add(directionalLight);																<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				전체 함수는 이렇습니다.
+			</p>
+
+			<!-- 2장 라이트 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						ambientLight = new THREE.AmbientLight(constants.defaultAmbientLightColor);	<br />
+						scene.add(ambientLight);
+					</td>
+				</tr>
+			</table>
+			<p>
+				앰비언트 라이트의 경우 생성할 때 색상만 지정해주면 끝입니다.<br />
+				위치의 영향을 받지 않는 라이트니까요.<br />
+				색상은 <code>constants</code>의 기본 색상을 이용하고 생성 후 바로 씬에 넣습니다.
+			</p>
+
+			<!-- 2장 라이트 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						directionalLight = new THREE.DirectionalLight(constants.defaultDirectionalLightColor);	<br />
+						directionalLight.position.set(0, constants.maxSunDistance, -constants.maxSunDistance);	<br />
+						scene.add(directionalLight);
+					</td>
+				</tr>
+			</table>
+			<p>
+				디렉셔널 라이트도 생성은 앰비언트 라이트와 같습니다.<br />
+				하지만 이 친구는 위치를 지정해줘야합니다.<br />
+				앞서 정의했던 <code>constants</code>에서 최대 거리 값을 넣어줍니다.<br />
+				씬에 넣는것도 잊지 마시고요.
+			</p>
+
+			<h3>2.8 오브젝트</h3>
+			<p>
+				마지막으로 <b>오브젝트</b> 배치입니다.<br />
+				만들어야 할 오브젝트는 총 세 종류인데요.<br />
+				각각 바닥, 태양, 나무입니다.
+			</p>
+
+			<!-- 2장 오브젝트 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createObjects() {																																				<br />
+						&nbsp; &nbsp; const loader = new OBJLoader();																															<br />
+						&nbsp; &nbsp; const whiteMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });																					<br />
+						&nbsp; &nbsp;																																							<br />
+						&nbsp; &nbsp; const bottomPlaneGeometry = new THREE.PlaneGeometry(1000, 1000);																							<br />
+						&nbsp; &nbsp; const bottomPlaneMesh = new THREE.Mesh(bottomPlaneGeometry, whiteMaterial);																				<br />
+						&nbsp; &nbsp; bottomPlaneMesh.rotation.x = THREE.MathUtils.degToRad(-90);																								<br />
+						&nbsp; &nbsp; scene.add(bottomPlaneMesh);																																<br />
+						&nbsp; &nbsp;																																							<br />
+						&nbsp; &nbsp; sunObject = new THREE.Mesh(																																<br />
+						&nbsp; &nbsp; constants.defaultSunObjectGeometry,																														<br />
+						&nbsp; &nbsp; constants.defaultSunObjectMaterial);																														<br />
+						&nbsp; &nbsp; sunObject.position.copy(directionalLight.position);																										<br />
+						&nbsp; &nbsp; sunObject.scale.multiplyScalar(40);																														<br />
+						&nbsp; &nbsp; scene.add(sunObject);																																		<br />
+						&nbsp; &nbsp;																																							<br />
+						&nbsp; &nbsp; for (let i = 0; i < constants.treeCount; ++i) {																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; loader.load('models/obj/tree.obj', function (tree) {																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tree.material = whiteMaterial;																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tree.position.set(																											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (i * constants.treeOffset) - (constants.treeOffset * (constants.treeCount / 2) - (constants.treeOffset / 2)),	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0,																												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (Math.random() - 0.5) * 2 * 100);																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tree.rotation.y = Math.random() * Math.PI;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tree.scale.multiplyScalar(100);																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; scene.add(tree);																												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; });																																			<br />
+						&nbsp; &nbsp; }																																							<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				조금 길지만 하나하나 살펴봅시다.
+			</p>
+
+			<!-- 2장 오브젝트 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						const loader = new OBJLoader();												<br />
+						const whiteMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 <code>loader</code>라는 오브젝트 로더를 생성합니다.<br />
+				<code>import</code>할 때 추가한 거 기억하시죠?<br />
+				그리고 흰색 재질을 생성하는데요, <code>THREE.MeshLambertMaterial()</code> 함수를 이용합니다.<br />
+				<b>램버트 재질</b>로 해야 빛을 받으니까요.<br />
+				색상도 흰색이니 <code>0xffffff</code>로 설정합니다.
+			</p>
+
+			<!-- 2장 오브젝트 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						const bottomPlaneGeometry = new THREE.PlaneGeometry(1000, 1000);			<br />
+						const bottomPlaneMesh = new THREE.Mesh(bottomPlaneGeometry, whiteMaterial);	<br />
+						bottomPlaneMesh.rotation.x = THREE.MathUtils.degToRad(-90);					<br />
+						scene.add(bottomPlaneMesh);
+					</td>
+				</tr>
+			</table>
+			<p>
+				바닥 오브젝트는 흰색 재질을 이용할 것이므로 지오메트리만 만들면 됩니다.<br />
+				<code>PlaneGeometry()</code> 함수를 이용해 크기가 <code>1000x1000</code>인 <b>평면 지오메트리</b>를 생성합니다.<br />
+				그리고 나서 지오메트리와 재질을 이용해 평면 오브젝트를 만듭니다.<br />
+				만들고 나면 기본적으로 바닥에 깔린 모양이 아니라 <b><code>z</code>의 양방향</b>을 바라보는 서 있는 모습이 됩니다.<br />
+				따라서 <code>x</code>축으로 <code>-90</code>도 돌려줘야 합니다(<code>90</code>도를 생각했다면 오른손/왼손 좌표계를 공부하고 오세요!).<br />
+				돌릴 때에는 <b>라디안 단위</b>를 사용하므로 <code>THREE.MathUtils.degToRad()</code> 함수를 사용해야 합니다.<br />
+				그리고 씬에 넣어주면 바닥 완성입니다.
+			</p>
+
+			<!-- 2장 오브젝트 코드 1-3 -->
+			<table>
+				<tr>
+					<td>
+						sunObject = new THREE.Mesh(							<br />
+						&nbsp; &nbsp; constants.defaultSunObjectGeometry,	<br />
+						&nbsp; &nbsp; constants.defaultSunObjectMaterial);	<br />
+						sunObject.position.copy(directionalLight.position);	<br />
+						sunObject.scale.multiplyScalar(40);					<br />
+						scene.add(sunObject);
+					</td>
+				</tr>
+			</table>
+			<p>
+				태양 오브젝트는 <code>constants</code>에서 정의한 기본 지오메트리 및 재질을 사용합니다.<br />
+				보시면 램버트가 아닌 <b>베이직 재질</b>임을 알 수 있습니다.<br />
+				이유는 당연히 태양이 디렉셔널 라이트의 영향을 받으면 어두워지기 때문입니다.<br />
+				따라서 태양을 항상 원하는 색으로 지정하기 위해 베이직 재질을 사용합니다.<br />
+				위치는 디렉셔널 라이트와 같은 위치로 합니다(<b>태양 = 디렉셔널 라이트</b>).<br />
+				크기는 너무 작으면 안 되므로 <code>x</code>, <code>y</code>, <code>z</code> 모든 요소에 <code>40</code>을 곱해줍니다.<br />
+				그리고 씬에 넣어주면 태양 오브젝트 완성입니다.
+			</p>
+
+			<!-- 2장 오브젝트 코드 1-4 -->
+			<table>
+				<tr>
+					<td>
+						for (let i = 0; i < constants.treeCount; ++i) {																										<br />
+						&nbsp; &nbsp; loader.load('models/tree.obj', function (tree) {																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; tree.material = whiteMaterial;																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; tree.position.set(																											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (i * constants.treeOffset) - (constants.treeOffset * (constants.treeCount / 2) - (constants.treeOffset / 2)),	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0,																											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (Math.random() - 0.5) * 2 * 100);																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; tree.rotation.y = Math.random() * Math.PI * 2;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; tree.scale.multiplyScalar(100);																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; scene.add(tree);																											<br />
+						&nbsp; &nbsp; });																																		<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				나무는 <code>for</code>문을 이용해서 만들어줍니다. 반복 수는 <code>constants</code>에서 설정한 나무 개수입니다.<br />
+				먼저 오브젝트 파일을 로드해야 하는데요, 먼저 <code>load()</code> 함수에 대해 알아봅시다.<br />
+				<code>load()</code> 함수는 4개의 인자를 받는데요.
+			</p>
+
+			<ol>
+				<li>모델 파일의 경로</li>
+				<li>로드 성공 시 콜백되는 함수</li>
+				<li>로드 과정 중 콜백되는 함수</li>
+				<li>로드 실패 시 콜백되는 함수</li>
+			</ol>
+			<p>
+				이렇게 구성됩니다.<br />
+				우리는 1번과 2번만 사용하면 되므로 3번 4번은 무시하셔도 됩니다.<br />
+				2번 함수의 경우 인자로 <code>tree</code>를 받죠? 이게 생성된 오브젝트입니다.<br />
+				그런데 우리는 흰색 재질을 나무에 적용시키고 싶으므로 함수 내부에서 값을 수정해줍니다.<br />
+				위치는 x의 경우 <b>스마트</b>하게 머릿속으로 계산해낸 공식을 사용하시면 되고요.<br />
+				<code>y</code>는 그대로 <code>0</code>, <code>z</code>는 <code>-100~100</code> 범위의 랜덤 값을 지정해줍니다(<code>Math.random()</code> 함수는 <code>-1~1</code> 범위의 랜덤 값을 반환합니다).<br />
+				각도도 <code>y</code>축으로 <code>0~360</code>도 범위의 랜덤 값으로 지정합니다.<br />
+				크기는 <code>100</code>을 곱해줍니다.<br />
+				이후 씬에 추가하면 나무도 끝!<br />
+				이제 방금까지 만들었던 생성 함수들을 <code>let</code> 선언 바로 아래에 쭉 호출해줍니다(<b>작성한 순서대로</b>).
+			</p>
+
+			<h3>2.9 업데이트 & 렌더 함수</h3>
+			<p>
+				그리고 실행을 위해 마지막으로 해줘야 할 것이 있죠. 바로 <code>animate()</code> 함수입니다.<br />
+				매 프레임마다 호출되는 그거요.
+			</p>
+
+			<!-- 2장 업데이트 & 렌더 함수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function animate() {										<br />
+						&nbsp; &nbsp; requestAnimationFrame(animate);				<br />
+						&nbsp; &nbsp;												<br />
+						&nbsp; &nbsp; update();										<br />
+						&nbsp; &nbsp; render();										<br />
+						&nbsp; &nbsp;												<br />
+						&nbsp; &nbsp; function update() {							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; stats.update();					<br />
+						&nbsp; &nbsp; }												<br />
+						&nbsp; &nbsp; function render() {							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; renderer.render(scene, camera);	<br />
+						&nbsp; &nbsp; }												<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				보시면 두 함수로 갈라져 있습니다.<br />
+				<code>update()</code> 함수와 <code>render()</code> 함수인데요.<br />
+				<code>update()</code> 함수는 매 프레임마다 실행할 <b>계산</b>을 담당합니다(돌아가는 오브젝트 만들때 각도 값을 더해주는 그런 계산입니다).<br />
+				<code>render()</code> 함수는 매 프레임마다 <b>렌더</b>하는 것만 담당합니다.
+			</p>
+
+			<!-- 2장 업데이트 & 렌더 함수 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						function update() {				<br />
+						&nbsp; &nbsp; stats.update();	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				업데이트 함수에서 스탯의 업데이트를 호출해줍시다.<br />
+				그래야 스탯이 매 프레임마다 정보를 보여줄 수 있습니다.
+			</p>
+
+			<!-- 2장 업데이트 & 렌더 함수 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						function render() {								<br />
+						&nbsp; &nbsp; renderer.render(scene, camera);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				렌더 함수에서는 장면을 렌더합니다.<br />
+				간단하죠?<br />
+				<code>run()</code> 함수에서 생성 함수 호출 부분 바로 아래에 <code>animate()</code> 함수를 호출해주도록 합시다.
+			</p>
+
+			<h3>2.10 정리 및 실행</h3>
+			<p>
+				거의 다 됐습니다!<br />
+				이제 <b>크기 조절 함수</b>만 만들면 실행해볼 수 있습니다.
+			</p>
+
+			<!-- 2장 정리 및 실행 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function onWindowResize() {												<br />
+						&nbsp; &nbsp; camera.aspect = window.innerWidth / window.innerHeight;	<br />
+						&nbsp; &nbsp; camera.updateProjectionMatrix();							<br />
+						&nbsp; &nbsp;															<br />
+						&nbsp; &nbsp; renderer.setSize(window.innerWidth, window.innerHeight);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				크기가 바뀌면 콜백되는 함수입니다.<br />
+				먼저 카메라 종횡비를 다시 설정하고 프로젝션 행렬을 재계산합니다.<br />
+				렌더러는 렌더 영역의 크기를 새로 업데이트합니다.
+			</p>
+
+			<!-- 2장 정리 및 실행 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						window.addEventListener('resize', onWindowResize);
+					</td>
+				</tr>
+			</table>
+			<p>
+				그리고 이 호출 구문을 <code>animate()</code> 함수 호출 아래에 놓아줍시다.<br />
+				이러면 화면 크기가 바뀌어도 정상적으로 크기가 업데이트되어 제대로 렌더링할 수 있습니다.<br />
+				마지막으로 스크립트 맨 아래에 <code>run()</code> 함수를 호출해줍니다.
+			</p>
+			<p>
+				음침한 황무지같은 풍경이 잘 보이시나요?<br />
+				결과물이 잘 보인다면 제대로 하신겁니다.
+			</p>
+		</details>
+
+		<p><mark>2장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_2.js" title="Light shaft chapter 2 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 3장 시작 -->
+
+	<div id="chapter_3">
+		<h2>3장: 카메라와 GUI 조작</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_3.png" alt="Chapter 3 result" title="Chapter 3 result"></p>
+
+		<details>
+			<summary><b>3장 튜토리얼 (펼치기/접기)</b></summary>
+			<p>
+				3장에서는 <b>카메라 조작</b>을 구현하고, <b>GUI</b>를 추가해서 세부적인 사항을 조절할 수 있도록 할 겁니다.<br />
+				사실 카메라의 경우 Three.js에서 <code>OrbitControls</code>라는 <del>날로먹는</del> 모듈을 제공해주기 때문에 굉장히 짧고 쉬울 예정입니다.<br />
+				<code>OrbitControls</code>에 대해 간단히 설명드리자면, 생성해서 카메라만 넣어주면 마우스로 카메라를 아주 손쉽게 조작할 수 있게 해주는 모듈입니다.<br />
+				피봇(<b>LookAt 위치</b>)을 중심으로 카메라를 이동시키므로 <code>OrbitControls</code>라는 이름이 붙은 듯 합니다.
+			</p>
+
+			<h3>3.1 모듈</h3>
+			<p>
+				먼저 2장에서 추가한 모듈 리스트에 <code>GUI</code>와 <code>OrbitControls</code> <b>모듈</b>을 추가해 줍시다.
+			</p>
+
+			<!-- 3장 모듈 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						import { GUI } from './jsm/libs/lil-gui.module.min.js';				<br />
+						import { OrbitControls } from './jsm/controls/OrbitControls.js';
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게요.
+			</p>
+
+			<h3>3.2 상수와 GUI 변수</h3>
+			<p>
+				오르빗 컨트롤에 필요한 <b>상수</b>도 조금 추가해보도록 하겠습니다.
+			</p>
+
+			<!-- 3장 상수와 GUI 변수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const constants = {									<br />
+						&nbsp; &nbsp; ~										<br />
+						&nbsp; &nbsp; orbitControlsMinDistance: 50,			<br />
+						&nbsp; &nbsp; orbitControlsMaxDistance: 500,		<br />
+						&nbsp; &nbsp; defaultSunColor: new Color(0xffffff),	<br />
+						&nbsp; &nbsp; ~										<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				이 셋을 아무 곳에나 추가해줍니다.<br />
+				먼저 <code>orbitControlsMinDistance</code>는 최대한 가까이 갈 수 있는 카메라와 피봇 간의 거리입니다.<br />
+				<code>orbitControlsMaxDistance</code>는 반대로 최대한 멀리 갈 수 있는 카메라와 피봇 간의 거리입니다.<br />
+				<code>defaultSunColor</code>는 이름만 봐도 알 수 있듯 태양 오브젝트의 색상입니다(<b>빛 색상과 혼동하지 말 것!</b>).
+			</p>
+			<p>
+				다음으로 해야할 일은 GUI에 사용할 <b>변수</b>를 등록하는 겁니다.<br />
+				<code>constants</code>와 모습이 꽤 비슷합니다.
+			</p>
+
+			<!-- 3장 상수와 GUI 변수 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						const controls = {																			<br />
+						&nbsp; &nbsp; backgroundColor: constants.defaultBackgroundColor, //렌더러의 바탕색				<br />
+						&nbsp; &nbsp; ambientLightColor: constants.defaultAmbientLightColor, // 앰비언트 라이트의 색상	<br />
+						&nbsp; &nbsp; sunLightColor: constants.defaultDirectionalLightColor, // 디렉셔널 라이트의 색상	<br />
+						&nbsp; &nbsp; sunLightIntensity: 1, // 디렉셔널 라이트의 강도									<br />
+						&nbsp; &nbsp; sunColor: constants.defaultSunColor, // 태양 오브젝트의 색상						<br />
+						&nbsp; &nbsp; sunPositionX: 0, // 태양 오브젝트의 x 위치										<br />
+						&nbsp; &nbsp; sunPositionY: constants.maxSunDistance, // 태양 오브젝트의 y 위치				<br />
+						&nbsp; &nbsp; sunPositionZ: -constants.maxSunDistance, // 태양 오브젝트의 z 위치				<br />
+						&nbsp; &nbsp; freeMove: false, // OrbitControls 사용 여부										<br />
+						&nbsp; &nbsp; fieldOfView: 60 // 카메라의 시야각												<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				나열해보니까 상당히 많네요.<br />
+				하지만 미리 많이 만들어두면 좋습니다.<br />
+				디렉셔널 라이트의 이름이 <code>sun</code>으로 바뀐 이유는 글자 수가 짧기 때문입니다.<br />
+				여기에 사용한 이름은 실제 <code>GUI</code>에서 보여질 이름이기 때문에, <code>directional</code>로 했다간 범위 밖으로 튀어나갈 수도 있습니다.<br />
+				각각이 무엇을 뜻하는지는 주석을 참고하세요.
+			</p>
+
+			<h3>3.3 오르빗 컨트롤 생성</h3>
+			<p>
+				먼저 <b>오르빗 컨트롤</b>을 먼저 생성하고 GUI를 만들도록 하겠습니다.<br />
+				가장 먼저 해야할 것은 역시 선언이겠죠?
+			</p>
+
+			<!-- 3장 오르빗 컨트롤 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let orbitControls;
+					</td>
+				</tr>
+			</table>
+			<p>
+				2장에서 만든 <code>let</code> 친구들 사이에 끼워넣어 줍니다.<br />
+				이제 <b>인스턴스</b>를 생성해봅시다.<br />
+				2장에서 했던 것처럼 생성 함수를 하나 만들어줍니다.
+			</p>
+
+			<!-- 3장 오르빗 컨트롤 생성 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function createOrbitControls() {												<br />
+						&nbsp; &nbsp; orbitControls = new OrbitControls(camera, renderer.domElement);	<br />
+						&nbsp; &nbsp; orbitControls.minDistance = constants.orbitControlMinDistance;	<br />
+						&nbsp; &nbsp; orbitControls.maxDistance = constants.orbitControlMaxDistance;	<br />
+						&nbsp; &nbsp; orbitControls.enabled = false;									<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; camera.lookAt(constants.defaultCameraLookAt);						<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				생성 시 카메라와 렌더러를 받습니다.<br />
+				생성한 후에는 방금 전 <code>constants</code>에 추가한 두 상수를 각각의 최소 거리와 최대 거리에 대입해줍니다.<br />
+				<code>enabled</code>는 오르빗 컨트롤을 사용할지 여부인데 저는 기본 값으로 <code>false</code>를 주었습니다.<br />
+				시작했을 땐 위를 바라보게 하기 위함입니다.<br />
+				여기서 중요한 부분이 있는데, 오르빗 컨트롤을 활성화하면 카메라는 반드시 오르빗 컨트롤에서 지정한 별도의 LookAt의 위치를 바라보게 됩니다.<br />
+				즉, 우리는 기본 값으로 <code>(0, 70, 0)</code>을 지정했지만 오르빗 컨트롤을 생성했기 때문에 카메라의 LookAt 값은 자동으로 <code>(0, 0, 0)</code>으로 바뀌어버렸습니다.<br />
+				그러므로 카메라의 LookAt 위치를 다시 기본 값으로 수정합니다.<br />
+				추가로 <code>createCamera()</code> 함수 내에서 LookAt 위치를 설정하는 구분은 지우셔도 됩니다.
+			</p>
+
+			<h3>3.4 GUI 생성</h3>
+			<p>
+				이제 <b>GUI</b>를 만들 차례입니다.<br />
+				지금부터 작성하는 코드는 모두 <code>createGUI()</code>를 만들어 안에 작성하면 됩니다.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {											<br />
+						&nbsp; &nbsp; const gui = new GUI();							<br />
+						&nbsp; &nbsp; const generalFolder = gui.addFolder('General');	<br />
+						&nbsp; &nbsp; const lightFolder = gui.addFolder('Light');		<br />
+						&nbsp; &nbsp; const cameraFolder = gui.addFolder('Camera');		<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				GUI는 인자 없이 생성할 수 있습니다.<br />
+				먼저 폴더를 만들어야 하는데, 저는 세 부분으로 나눴습니다.<br />
+				그 다음엔 원하는 항목을 추가합니다.<br />
+				일단 렌더러의 배경색으로 해보죠. 라이트와는 관계 없으므로 <code>General</code> 항목에 넣겠습니다.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						generalFolder.addColor(controls, 'backgroundColor').listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; renderer.setClearColor(value, 1);												<br />
+						});
+					</td>
+				</tr>
+			</table>
+			<p>
+				뭔가 복잡하죠?<br />
+				세 가지의 명령을 한 번에 실행해서 그렇습니다. 한 번 나눠보죠.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 3 -->
+			<table>
+				<tr>
+					<td>
+						const category = generalFolder.addColor(controls, 'backgroundColor');
+					</td>
+				</tr>
+			</table>
+			<p>
+				이건 <code>General</code> 폴더에 색상을 설정할 수 있는 항목을 넣겠다는 뜻입니다.<br />
+				인자로 <code>controls</code>와 <code>'backgroundColor'</code>를 넣은 것은 <code>controls.backgroundColor</code> 값이 여기에 영향을 받을 것이라는 의미입니다.<br />
+				이름을 꼭 똑같이 넣어줘야 에러가 나지 않습니다!
+			</p>
+
+			<!-- 3장 GUI 생성 코드 4 -->
+			<table>
+				<tr>
+					<td>
+						category.listen();
+					</td>
+				</tr>
+			</table>
+			<p>
+				이것은 <code>controls.backgroundColor</code> 값이 GUI 조작 없이 바뀌었을 때 GUI 디스플레이에 바뀐 값을 표시해주겠다는 뜻입니다.<br />
+				이걸 안 하시면 바뀌어도 GUI 디스플레이엔 바뀌기 전의 값이 계속 표시됩니다.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 5 -->
+			<table>
+				<tr>
+					<td>
+						category.onChange(function (value) {			<br />
+						&nbsp; &nbsp; renderer.setClearColor(value, 1);	<br />
+						});
+					</td>
+				</tr>
+			</table>
+			<p>
+				이건 GUI 조작으로 인해 값이 바뀌었을 때 콜백되는 함수를 설정하는 겁니다.<br />
+				바뀐 값이 <code>value</code>로서 들어오게 됩니다.<br />
+				그 값을 이용해 배경색을 바꾸면 되겠죠?<br />
+				이런 식으로 세 가지의 함수 호출을 한 줄로 줄인 것이 저겁니다.<br />
+				저렇게 하지 않으면 <code>category</code>처럼 오브젝트를 하나씩 만들어줘야하고, 줄 수도 늘어납니다.<br />
+				이제 더 추가해볼까요?
+			</p>
+
+			<!-- 3장 GUI 생성 코드 6 -->
+			<table>
+				<tr>
+					<td>
+						lightFolder.addColor(controls, 'ambientLightColor').listen().onChange(function (value) {											<br />
+						&nbsp; &nbsp; ambientLight.color = value;																							<br />
+						});																																	<br />
+						lightFolder.addColor(controls, 'sunLightColor').listen().onChange(function (value) {												<br />
+						&nbsp; &nbsp; directionalLight.color = value;																						<br />
+						});																																	<br />
+						lightFolder.add(controls, 'sunLightIntensity', 0, 1).listen().onChange(function (value) {											<br />
+						&nbsp; &nbsp; directionalLight.intensity = value;																					<br />
+						});																																	<br />
+						lightFolder.addColor(controls, 'sunColor').listen().onChange(function (value) {														<br />
+						&nbsp; &nbsp; sunObject.material.color = value;																						<br />
+						})																																	<br />
+						lightFolder.add(controls, 'sunPositionX', -constants.maxSunDistance, constants.maxSunDistance).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; directionalLight.position.x = value;																					<br />
+						&nbsp; &nbsp; sunObject.position.x = value;																							<br />
+						});																																	<br />
+						lightFolder.add(controls, 'sunPositionY', -constants.maxSunDistance, constants.maxSunDistance).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; directionalLight.position.y = value;																					<br />
+						&nbsp; &nbsp; sunObject.position.y = value;																							<br />
+						});																																	<br />
+						lightFolder.add(controls, 'sunPositionZ', -constants.maxSunDistance, constants.maxSunDistance).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; directionalLight.position.z = value;																					<br />
+						&nbsp; &nbsp; sunObject.position.z = value;																							<br />
+						});
+					</td>
+				</tr>
+			</table>
+			<p>
+				대부분이 <code>Light</code> 폴더에 들어갑니다.<br />
+				색상은 해봤으니 알겠고, 그럼 슬라이더는 어떻게 만드는지 알아봅시다.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 6-1 -->
+			<table>
+				<tr>
+					<td>
+						lightFolder.add(controls, 'sunLightIntensity', 0, 1).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; directionalLight.intensity = value;											<br />
+						});
+					</td>
+				</tr>
+			</table>
+			<p>
+				일단 함수명이 <code>addColor</code>에서 <code>add</code>로 바뀌었습니다. 인자도 2개 늘어났네요.<br />
+				인자는 각각 최솟값과 최댓값입니다. 이 경우 <code>0~1</code> 범위가 되겠군요.<br />
+				나머지는 색상하고 같습니다.<br />
+				이제 나머지 항목들도 어떤 원리인지 아시겠죠?<br />
+				<code>sunPositionX/Y/Z</code>의 경우 디렉셔널 라이트와 태양 오브젝트는 꼭 붙어다녀야 하는 친구들이기에 같이 변경해주었습니다.<br />
+				마지막으로 카메라만 하면 끝입니다.
+			</p>
+
+			<!-- 3장 GUI 생성 코드 7 -->
+			<table>
+				<tr>
+					<td>
+						cameraFolder.add(controls, 'freeMove').listen().onChange(function (value) {				<br />
+						&nbsp; &nbsp; camera.position.copy(constants.defaultCameraPosition);					<br />
+						&nbsp; &nbsp;																			<br />
+						&nbsp; &nbsp; if (value) {																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; camera.lookAt(0, 0, 0);										<br />
+						&nbsp; &nbsp; } else {																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; camera.lookAt(constants.defaultCameraLookAt);				<br />
+						&nbsp; &nbsp; }																			<br />
+						&nbsp; &nbsp;																			<br />
+						&nbsp; &nbsp; orbitControls.enabled = value;											<br />
+						});																						<br />
+						cameraFolder.add(controls, 'fieldOfView', 40, 80).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; camera.fov = value;														<br />
+						&nbsp; &nbsp; camera.updateProjectionMatrix();											<br />
+						});
+					</td>
+				</tr>
+			</table>
+			<p>
+				카메라는 항목이 두 개입니다.<br />
+				먼저 <code>freeMove</code>인데요. 이걸 활성화하면 오르빗 컨트롤이 활성화됩니다.<br />
+				활성화라고 하니 당연히 체크박스겠죠? 체크박스는 슬라이더에서 최솟값과 최댓값 자리를 비우면 만들 수 있습니다.<br />
+				내용을 보면 일단 <code>true/false</code> 상관 없이 카메라의 위치를 기본 위치로 바꿉니다.<br />
+				그리고 <code>true</code>라면 <b>LookAt 위치</b>를 원점으로, <code>false</code>라면 기본 위치로 바꿉니다.<br />
+				끝으로 오르빗 컨트롤의 <code>enabled</code> 값을 <code>value</code>로 설정해주면 끝!<br />
+				시야각의 경우 <code>40~80</code> 범위를 갖는 슬라이더이며 카메라의 <code>fov</code> 값 수정 후 <code>updateProjectionMatrix()</code> 함수를 호출해줍니다.<br />
+				이걸 해 주지 않으면 바꿔도 변화가 없으므로 주의하세요.<br />
+				이제 GUI도 끝났습니다.
+			</p>
+			<p>
+				실행해보면 GUI를 조작해 카메라 및 조명 등 많은 것을 조절할 수 있는 것을 확인할 수 있습니다.<br />
+				결과물이 잘 보인다면 제대로 하신겁니다.
+			</p>
+		</details>
+
+		<p><mark>3장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_3.js" title="Light shaft chapter 3 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 4장 시작 -->
+
+	<div id="chapter_4">
+		<h2>4장: 렌더 타겟</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_4.png" alt="Chapter 4 result" title="Chapter 4 result"></p>
+
+		<details>
+			<summary><b>4장 튜토리얼 (펼치기/접기)</b></summary>
+
+			<h3>4.1 렌더 타겟이란?</h3>
+			<p>
+				이제부터 약간의 원리 이해가 필요합니다.<br />
+				먼저 <b>렌더 타겟</b>에 대해 설명드리도록 하겠습니다.<br />
+				렌더 타겟이란 렌더링의 대상을 말합니다. 즉, 정육면체를 렌더했으면 그것은 렌더 타겟이라는 버퍼에 그려지게 됩니다.<br />
+				근데 생각해보니 우린 이제까지 렌더 타겟을 지정해준 적이 없죠? 화면에는 잘 나오던데, 왜 그럴까요?<br />
+				답은 <b>기본 렌더 타겟</b>이 있기 때문입니다. 렌더 타겟에는 <b>기본 렌더 타겟</b>이 있고 <b>사용자 지정 렌더 타겟</b>이 있는데요.<br />
+				우리는 지금까지 별도로 렌더 타겟을 만든 적이 없으니 그대로 기본 렌더 타겟에 그려지고 있던 겁니다.<br />
+				하지만 지금부터는 별도의 렌더 타겟이 필요합니다.<br />
+				추가적인 렌더 타겟이 왜 필요한지도 알아야겠죠? 답은 <b>영상 처리의 원리</b>에 있습니다.<br />
+				영상 처리란 오브젝트를 전부 그린 버퍼를 <b>후처리(Post-processing)</b>하는 작업을 말합니다.<br />
+				예를 들어 장면 전체를 흑백 화면으로 모니터에 출력하려면 어떻게 하면 될까요?<br />
+				모든 오브젝트의 재질 색을 흑백으로 바꾸는 건 현명하지 않겠죠?<br />
+				그럼 그냥 컬러로 모든 오브젝트를 그리고 이후 그 렌더 타겟을 따로 빼서 흑백 처리를 해주면 어떨까요.<br />
+				그럼 더 효율적이겠죠? 이것이 영상 처리 기법입니다.
+			</p>
+			<p>
+				라이트 셰프트를 구현하기 위해서는 장면의 모든 오브젝트가 그려진 렌더 타겟에서, 오브젝트가 없는 부분을 추출해야만 합니다.<br />
+				왜인지는 복잡하므로 지금 설명하지 않겠습니다. 6장까지 보시면 알 수 있습니다.<br />
+				따라서 우리는 새로운 렌더 타겟을 하나 만들고 거기에 모든 오브젝트를 그려야 합니다.
+			</p>
+
+			<h3>4.2 렌더 타겟 생성</h3>
+			<p>
+				먼저 선언부터 해볼까요.
+			</p>
+
+			<!-- 4장 렌더 타겟 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let originalRT;
+					</td>
+				</tr>
+			</table>
+			<p>
+				이름은 원래 장면을 담을 것이므로 <code>originalRT</code>로 했습니다.<br />
+				<code>RT</code>는 당연히 <code>RenderTarget</code>의 약자입니다.<br />
+				다음은 생성입니다.<br />
+				생성 방법은 그리 거창하지 않습니다.<br />
+				이전처럼 함수를 따로 만들어 줍시다.
+			</p>
+
+			<!-- 4장 렌더 타겟 생성 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function createRenderTargets() {																<br />
+						&nbsp; &nbsp; originalRT = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);	<br />
+						&nbsp; &nbsp; originalRT.depthTexture = new THREE.DepthTexture();								<br />
+						&nbsp; &nbsp; originalRT.depthTexture.format = THREE.DepthFormat;								<br />
+						&nbsp; &nbsp; originalRT.depthTexture.type = THREE.UnsignedShortType;							<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				생성 함수에 크기만 넣어주면 끝입니다. 하지만 아래에 뭔가 더 있죠?<br />
+				바로 <b>깊이(depth) 텍스처</b>입니다. 원래 안 넣으면 <code>null</code>로 설정되어 제공해주지 않지만, 우린 이게 꼭 필요합니다.<br />
+				이유는 역시 복잡하므로 일단 넘어갑니다. 5장에서 사용할 거예요.<br />
+				깊이 텍스처를 생성했으면 텍스처의 형식을 지정해줘야 합니다.<br />
+				깊이를 저장할 것이므로 형식은 <code>THREE.DepthFormat</code>으로 해줍니다.<br />
+				타입은 버퍼의 자료형을 말합니다. 버퍼는 하나의 배열이므로 한 칸의 크기를 지정해줘야 합니다.<br />
+				하지만 깊이는 별 거 없죠? 본래 <code>0~1</code> 범위의 값이므로 2바이트면 충분할 겁니다. 따라서 <code>THREE.UnsignedShortType</code>을 지정해줍니다.<br />
+				렌더 타겟이 완성되었습니다!<br />
+				이 함수를 호출하는 것을 잊지 마세요. <code>createLights()</code> 함수 바로 아래에서 호출해주시면 됩니다.<br />
+				그럼 이제 장면을 여기에 렌더하도록 바꿔봅시다.
+			</p>
+
+			<h3>4.3 렌더 타겟 변경</h3>
+			<p>
+				변경하는 방법도 엄청 쉽습니다.<br />
+				<code>animate()</code> 함수의 <code>render()</code> 함수로 가봅시다.
+			</p>
+
+			<!-- 4장 렌더 타겟 변경 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function render() {									<br />
+						&nbsp; &nbsp; renderer.setRenderTarget(originalRT);	<br />
+						&nbsp; &nbsp; renderer.render(scene, camera);		<br />
+						&nbsp; &nbsp; renderer.setRenderTarget(null);		<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>render()</code> 함수 위 아래에 <code>setRenderTarget()</code> 함수를 호출해주면 됩니다.<br />
+				인자는 각각 <code>originalRT</code>와 <code>null</code>인데요.<br />
+				<code>originalRT</code>는 방금 만든 렌더 타겟이고, <code>null</code>은 기본 렌더 타겟입니다(알아서 그렇게 인식합니다).<br />
+				즉, <code>originalRT</code>에 렌더하고 렌더 후 기본 렌더 타겟으로 다시 되돌려놓는 겁니다.<br />
+				되돌려 놓는 것이 필수는 아니지만 그렇게 해놓으면 깔끔하고 좋습니다.
+			</p>
+
+			<h3>4.4 자동 크기 조절</h3>
+			<p>
+				렌더 타겟은 크기가 정해진 텍스처와 같습니다.<br />
+				그렇다는 말은 창의 크기가 변경되면 렌더 타겟의 크기도 업데이트해줘야 한다는 겁니다.<br />
+				<code>onWindowResize()</code> 함수를 조금만 수정해봅시다.
+			</p>
+
+			<!-- 4장 자동 크기 조절 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function onWindowResize() {													<br />
+						&nbsp; &nbsp; camera.aspect = window.innerWidth / window.innerHeight;		<br />
+						&nbsp; &nbsp; camera.updateProjectionMatrix();								<br />
+						&nbsp; &nbsp;																<br />
+						&nbsp; &nbsp; renderer.setSize(window.innerWidth, window.innerHeight);		<br />
+						&nbsp; &nbsp; originalRT.setSize(window.innerWidth, window.innerHeight);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				맨 아래에 <code>originalRT</code>의 크기를 업데이트하도록 해주면 됩니다.<br />
+				그럼 이제 실행해볼까요?<br />
+				네, 하나도 안 보이면 <b>정상</b>입니다!<br />
+				우린 방금 기본 렌더 타겟인 화면이 아닌 <code>originalRT</code>에 렌더링 했으니까요.<br />
+				궁금하면 <code>render()</code> 함수에서 <code>originalRT</code>로 렌더 타겟을 변경하는 줄을 주석 처리해보세요.<br />
+				다시 잘 보이죠?
+			</p>
+			<p>
+				주석 처리한 부분은 다시 원래대로 되돌려놔야 합니다.
+				결과물이 잘 <del>안</del>보인다면 제대로 하신겁니다.
+			</p>
+		</details>
+
+		<p><mark>4장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_4.js" title="Light shaft chapter 4 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 5장 시작 -->
+
+	<div id="chapter_5">
+		<h2>5장: 가중치 추출</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_5.png" alt="Chapter 5 result" title="Chapter 5 result"></p>
+
+		<details>
+			<summary><b>5장 튜토리얼 (펼치기/접기)</b></summary>
+			<p>
+				5장부터는 꽤 어려워지기 시작합니다.<br />
+				아마 모르거나 처음 보는 내용이 많을 거예요.<br />
+				일단 모듈부터 추가해보죠.
+			</p>
+
+			<h3>5.1 모듈</h3>
+			<p>
+				이 프로그램에 추가될 마지막 <b>모듈</b> 둘입니다.
+			</p>
+
+			<!-- 5장 모듈 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						import { EffectComposer } from './jsm/postprocessing/EffectComposer.js';	<br />
+						import { ShaderPass } from './jsm/postprocessing/ShaderPass.js';
+					</td>
+				</tr>
+			</table>
+			<p>
+				첫 번재 것은 <b>이펙트 컴포저</b>라고하는 것인데요. 한 번에 여러 <b>패스</b>를 사용할 수 있도록 해줍니다.<br />
+				여기서 잠깐, 패스부터 설명하겠습니다. 패스란 한 번의 <b>렌더링</b>을 뜻합니다.<br />
+				예를 들어 4장에서 봤던 흑백 효과 적용을 예로 들어봅시다.<br />
+				이 경우 전체 장면을 <code>originalRT</code>같은 곳에 그리는 <b>1번 패스</b>가 있습니다.<br />
+				그 다음엔 <code>originalRT</code>를 요리조리 바꿔서 흑백으로 만드는 <b>2번 패스</b>가 있겠죠.<br />
+				이펙트 컴포저는 여기서 1번 패스 2번 패스를 이펙트 컴포저에 <b>등록한 순으로 실행</b>해주는 친구입니다.<br />
+				사실 여러 패스가 아니라 단일 패스더라도 이펙트 컴포저가 없으면 <code>ShaderPass</code>를 렌더하기 어렵습니다.<br />
+				두 번째 것은 <b>셰이더 패스</b>입니다. 정확히는 영상 처리용 패스라고 보는 것이 좋습니다.<br />
+				여기서 또 다른 의문점이 해결됩니다. 위에서 언급한 흑백 효과를 잘 적용했다고 칩시다.<br />
+				그럼 그 <code>originalRT</code>에 흑백이 적용된 내용을 기본 렌더 타겟에 제대로 렌더해줘야 하지 않겠어요?<br />
+				하지만 어떻게 하죠? <code>originalRT</code>는 하나의 평면입니다. 삼각형 두 개가 만나있는 그 간단한 평면입니다.<br />
+				이걸 어떻게 해야 카메라 면전에 딱 맞게 들이밀 수 있을까요?<br />
+				바로 <b>화면에 정렬된 평면(Screen-aligned quad)</b>를 이용하는 겁니다.<br />
+				그 전에 먼저 <b>프로젝션 공간</b>을 이해해야 합니다. 사실 이해라 할 것도 없어요.
+			</p>
+
+			<p><img src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/references/projection_vs_uv.png" alt="Projection vs UV" title="Projection vs UV"></p>
+
+			<p>
+				그림을 보면 아시다시피 프로젝션 공간은 왼쪽과 아래쪽 끝이 <code>-1</code>이고, 오른쪽과 위쪽 끝이 <code>1</code>입니다.<br />
+				그리고 버텍스 셰이더에서는 기본적으로 <b>월드 공간</b>에 있는 버텍스를 <b>프로젝션 공간</b>으로 변환시키죠.<br />
+				그럼 그냥 월드 공간의 위치가 <code>-1~1</code>로 된 평면을 버텍스 셰이더에서 뷰 행렬 및 프로젝션 행렬을 곱하지 않고 다이렉트로 넘기면 되는 거 아닐까요?<br />
+				그럼 월드 공간에서 <code>-1~1</code>이었던 버텍스가 프로젝션 공간에서도 그대로 <code>-1~1</code>이게 되고, 이건 화면에 꼭 맞는 평면인 거죠!<br />
+				예... 그래서 그걸 직접 하실 필요는 없고요. 우리 믿음직한 셰이더 패스 친구가 다 해준답니다.<br />
+				참고로 그림에 있는 <b>UV 공간</b>도 기억해두시기 바랍니다. 얘는 <code>-1</code>이 아니라 <code>0</code>부터 시작합니다. 6장에서 분명 다시 보게 될 거예요.
+			</p>
+
+			<h3>5.2 가중치 렌더 타겟 생성</h3>
+			<p>
+				모듈도 불러왔고 하니 이제 가중치를 저장해놓을 렌더 타겟을 추가로 선언해봅시다.
+			</p>
+
+			<!-- 5장 가중치 렌더 타겟 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let weightRT;
+					</td>
+				</tr>
+			</table>
+			<p>
+				이름은 <code>weightRT</code>입니다.<br />
+				렌더 타겟 생성은 기존의 <code>createRenderTargets()</code> 함수에 추가합니다.
+			</p>
+
+			<!-- 5장 가중치 렌더 타겟 생성 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function createRenderTargets() {																<br />
+						&nbsp; &nbsp; originalRT = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);	<br />
+						&nbsp; &nbsp; originalRT.depthTexture = new THREE.DepthTexture();								<br />
+						&nbsp; &nbsp; originalRT.depthTexture.format = THREE.DepthFormat;								<br />
+						&nbsp; &nbsp; originalRT.depthTexture.type = THREE.UnsignedShortType;							<br />
+						&nbsp; &nbsp;																					<br />
+						&nbsp; &nbsp; weightRT = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				단 한 줄이면 됩니다.<br />
+				가중치 렌더 타겟은 깊이 텍스처가 필요 없거든요.
+			</p>
+
+			<h3>5.3 가중치 추출 셰이더</h3>
+			<p>
+				이제 5장에서 가장 어려운 파트입니다.<br />
+				<b>셰이더</b>를 직접 짜보는 것이죠.<br />
+				만들 셰이더는 <code>originalRT</code>의 깊이 텍스처를 가지고 가중치를 계산하는 겁니다.<br />
+				대체 뭔 가중치일까요? 바로 <b>라이트 셰프트의 강도 가중치</b>입니다.<br />
+				이 가중치를 바탕으로 그 픽셀에 들어갈 라이트 셰프트의 강도가 정해집니다.<br />
+				그럼 이거부터 알아야겠네요. 장면의 어느 부분이 가장 가중치가 높을까요? 정답은 <b>오브젝트가 없는 부분</b>입니다.<br />
+				게임이나 현실에서 빛 줄기를 보면 어떻죠? 오브젝트가 없으면 완전 진하다가 오브젝트에 닿으면 점점 감쇄가 되죠?<br />
+				그래서 빛 줄기가 오브젝트가 없는 곳으로는 쭉 뻗어나가는 거죠. 오브젝트가 있다면 점점 감쇄되다가 이내 사라집니다.<br />
+				즉, 오브젝트가 없는 부분엔 특정 가중치를 주고, 없는 부분은 <code>0</code> 값이나 넣어버리면 되는 겁니다.<br />
+				어... 그러면 오브젝트가 있는지 없는지는 어떻게 알까요? <code>originalRT</code>를 보고 검은색은 오브젝트가 없다!로 치면 될까요?<br />
+				아니죠, 그러면 검은색을 가진 오브젝트는 있음에도 불구하고 없는 것으로 처리되고 맙니다.<br />
+				따라서 색상이 아닌 다른 무언가를 보고 오브젝트가 있는지 없는지를 판단해야해요.<br />
+				답은 바로 <b>깊이</b>입니다.<br />
+				깊이 텍스처는 색상에 관계없이 오브젝트가 없으면 <code>1</code>, 있으면 카메라에 얼마나 가깝냐에 따라 <code>0~1</code>까지의 값을 넣어줍니다.<br />
+				이제 어떻게 해야할지 아시겠지요?<br />
+				<code>originalRT</code>의 깊이 텍스처를 몰래 염탐해서 <code>1</code>이면 가중치를(흰색 또는 회색) 그리고, <code>1</code>이 아니면 <code>0</code>을(검은색)을 그려주면 됩니다.<br />
+				설명은 이만하고 코드부터 봅시다.
+			</p>
+
+			<!-- 5장 가중치 추출 셰이더 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const WeightShader = {																								<br />
+						&nbsp; &nbsp; uniforms: {																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'depthMap': { value: null }																<br />
+						&nbsp; &nbsp; },																									<br />
+						&nbsp; &nbsp;																										<br />
+						&nbsp; &nbsp; vertexShader: `																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; varying vec2 texCoord;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; void main() {																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; texCoord = uv;															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }`,																						<br />
+						&nbsp; &nbsp;																										<br />
+						&nbsp; &nbsp; fragmentShader: `																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; varying vec2 texCoord;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform sampler2D depthMap;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; void main() {																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float depth = texture2D(depthMap, texCoord).x;							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float weight = (depth == 1.0) ? 0.3 : 0.0;								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gl_FragColor = vec4(weight, weight, weight, 1.0);							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }`																						<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				문법은 엄청 복잡하지는 않습니다. 천천히 보도록 하죠.
+			</p>
+
+			<!-- 5장 가중치 추출 셰이더 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						uniforms: {									<br />
+						&nbsp; &nbsp; 'depthMap': { value: null }	<br />
+						},
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 <code>uniforms</code> 리스트는 <b>프로그램으로부터 받아올 오브젝트들의 모음집</b>입니다.<br />
+				셰이더는 GPU에서 실행되므로 이렇게 <code>uniforms</code>라는 가방에 담아서 넘겨줘야 셰이더 내에서 사용할 수 있습니다.<br />
+				지금같은 경우는 <code>originalRT</code>의 깊이 텍스처인 <code>depthMap</code>을 넘겨줍니다. <code>value</code>는 값을 설정하지 않았을 때의 기본 값입니다.
+			</p>
+
+			<!-- 5장 가중치 추출 셰이더 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						vertexShader: `													<br />
+						&nbsp; &nbsp; varying vec2 texCoord;							<br />
+						&nbsp; &nbsp;													<br />
+						&nbsp; &nbsp; void main() {										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; texCoord = uv;						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; gl_Position = vec4(position, 1.0);	<br />
+						&nbsp; &nbsp; }`,
+					</td>
+				</tr>
+			</table>
+			<p>
+				다음은 <b>버텍스 셰이더</b>입니다.<br />
+				아까 셰이더 패스가 알아서 화면에 꼭 맞는 평면을 준비해준다고 했죠?<br />
+				그러므로 여기서 할 일은 없습니다.<br />
+				포지션에 뷰/프로젝션 행렬을 곱해줄 필요가 없다는 뜻입니다. 그대로 <code>gl_Position</code>에 4차원 벡터화해서 대입해줍니다.<br />
+				다만 프래그먼트 셰이더에서 <code>depthMap</code>을 추출해야하므로 <code>texCoord</code>라는 UV 값도 그대로 넘겨줍니다.<br />
+				<code>texCoord</code>의 앞에 <code>varying</code> 키워드가 있어야 프래그먼트 셰이더로 값을 넘겨줄 수 있습니다.<br />
+				프래그먼트 셰이더로 값이 넘어간다는 말은, 그 값은 반드시 <b>래스터라이제이션</b>이 된다는 의미입니다.<br />
+				즉, 이제 픽셀(프래그먼트) 하나하나가 버텍스에 따라 보간된 <b>고유한 <code>texCoord</code> 값</b>을 갖게 됩니다.
+			</p>
+
+			<!-- 5장 가중치 추출 셰이더 코드 1-3 -->
+			<table>
+				<tr>
+					<td>
+						fragmentShader: `																<br />
+						&nbsp; &nbsp; varying vec2 texCoord;											<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; uniform sampler2D depthMap;										<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; void main() {														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float depth = texture2D(depthMap, texCoord).x;		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float weight = (depth == 1.0) ? 0.3 : 0.0;			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; gl_FragColor = vec4(weight, weight, weight, 1.0);	<br />
+						&nbsp; &nbsp; }`
+					</td>
+				</tr>
+			</table>
+			<p>
+				<b>프래그먼트 셰이더</b>입니다.<br />
+				보시면 텍스처의 경우 <code>sampler2D</code>라는 자료형입니다.<br />
+				추가로 <code>uniforms</code>에서 왔다는 의미로 <code>uniform</code> 키워드를 붙여서 선언해야 합니다.<br />
+				함수를 보면 간단합니다. <code>texture2D()</code> 함수를 이용해 깊이 텍스처로부터 값을 추출합니다.<br />
+				추출한 결과의 자료형은 RGBA를 모두 갖고 있으므로 <code>vec4</code>가 됩니다.<br />
+				하지만 우리가 필요한 것은 흰색이냐 아니냐죠? 아까 오브젝트가 없는 부분이 흰색이라고 했으니까요.<br />
+				그렇다면 굳이 RGBA 값을 다 볼 필요가 없죠? 흰색이면 RGB 값이 모두 똑같으니 말이죠.<br />
+				그러므로 <code>.x</code>를 붙여서 빨간색만 가져옵시다. <code>vec4</code>가 <code>float</code>으로 줄었으니 편하군요.<br />
+				이제 추출한 <code>depth</code> 값이 <code>1</code>인지 검사합니다. 삼항 연산자를 사용하면 좋습니다.<br />
+				저는 오브젝트가 없다면 <code>0.3</code>을 주었습니다.<br />
+				예상 외로 조금 약하죠? 하지만 이보다 커지면 라이트 셰프트가 너무 강해지게 됩니다.<br />
+				마지막으로 <code>gl_FragColor</code>에 <code>weight</code> 값으로 이뤄진 <code>vec4</code>를 던져주면 됩니다.<br />
+				아마 검은색과 어두운 회색이 섞인 느낌의 텍스처가 탄생할 듯 합니다.<br />
+				조금 힘들었지만 어떻게든 셰이더를 짜냈습니다!<br />
+				이제 이걸 사용해줄 패스와 이펙트 컴포저를 생성하러 가봅시다.
+			</p>
+
+			<h3>5.4 이펙트 컴포저 및 패스 생성</h3>
+			<p>
+				다시 쉬운 파트입니다.<br />
+				먼저 선언부터 하겠습니다.
+			</p>
+
+			<!-- 5장 이펙트 컴포저 및 패스 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let weightComposer;	<br />
+						let weightPass;
+					</td>
+				</tr>
+			</table>
+			<p>
+				사용할 패스는 하나입니다.<br />
+				하나일지라도 이펙트 컴포저는 꼭 필요하므로 넣어주세요.<br />
+				패스 생성 함수를 추가합시다.<br />
+				여기서 이펙트 컴포저도 생성합니다.
+			</p>
+
+			<!-- 5장 이펙트 컴포저 및 패스 생성 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function createPasses() {														<br />
+						&nbsp; &nbsp; weightComposer = new EffectComposer(renderer, weightRT);			<br />
+						&nbsp; &nbsp; weightComposer.renderToScreen = false;							<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; weightPass = new ShaderPass(WeightShader);						<br />
+						&nbsp; &nbsp; weightPass.uniforms['depthMap'].value = originalRT.depthTexture;	<br />
+						&nbsp; &nbsp; weightComposer.addPass(weightPass);								<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 <b>이펙트 컴포저</b>부터 만듭니다.<br />
+				인자로 렌더러와 렌더 타겟을 받습니다. 만약 렌더 타겟을 넣지 않는다면 기본 렌더 타겟으로 설정됩니다.<br />
+				정확히는 <code>renderToScreen</code>을 <code>false</code>로 해줘야만 <code>weightRT</code>에 렌더하게 됩니다.<br />
+				이걸 <code>false</code>로 하지 않으면 <b>무조건 기본 렌더 타겟에 렌더</b>하므로 주의하세요!<br />
+				다음은 <b>패스</b>입니다. 셰이더 패스는 인자로 <b>셰이더 코드</b>를 받습니다.<br />
+				우리가 정성스레 빚은 셰이더 코드를 전해줍시다.<br />
+				이후 <code>uniforms</code>도 채워줘야 합니다. 아까 제가 셰이더에 들고갈 가방이라고 말했죠?<br />
+				<code>depthMap</code>에 <code>originalRT</code>의 깊이 텍스처를 넣어주면 됩니다.<br />
+				그리고 이펙트 컴포저에 패스를 추가하면 끝입니다.<br />
+				이 함수는 <code>createRenderTargets()</code> 함수 아래에서 호출하도록 해주시면 되겠습니다.
+			</p>
+
+			<h3>5.5 자동 크기 조절</h3>
+			<p>
+				실행하기 전에 손 볼 부분이 조금 있습니다.<br />
+				4장 때처럼 <code>onWindowResize()</code> 함수를 수정해줘야합니다.
+			</p>
+
+			<!-- 5장 자동 크기 조절 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function onWindowResize() {														<br />
+						&nbsp; &nbsp; camera.aspect = window.innerWidth / window.innerHeight;			<br />
+						&nbsp; &nbsp; camera.updateProjectionMatrix();									<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; renderer.setSize(window.innerWidth, window.innerHeight);			<br />
+						&nbsp; &nbsp; originalRT.setSize(window.innerWidth, window.innerHeight);		<br />
+						&nbsp; &nbsp; weightComposer.setSize(window.innerWidth, window.innerHeight);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				맨 아래에 <code>weightComposer</code>의 크기를 업데이트하도록 만들어주세요.<br />
+				<code>weightRT</code>를 직접 조절하지 않아도 <code>weightComposer</code>의 <code>setSize()</code> 함수를 사용하면 알아서 크기가 바뀝니다.<br />
+				이제 실행하면 어떻게 될까요?<br />
+				역시나 아무것도 보이지 않습니다. 어떻게 볼 수 있는 방법이 있을까요?<br />
+				답은 아주 간단합니다. 이펙트 컴포저 생성 부분에서 <code>renderToScreen</code>을 <code>false</code>로 하는 줄을 <b>주석</b>처리하면 됩니다.<br />
+				그럼 이제 가중치 텍스처를 볼 수 있습니다!
+			</p>
+			<p>
+				확인했으면 4장 처럼 다시 되돌리는 것을 잊지 맙시다.<br />
+				결과물이 마찬가지로 잘 <del>안</del>보인다면 제대로 하신겁니다.
+			</p>
+		</details>
+
+		<p><mark>5장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_5.js" title="Light shaft chapter 5 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 6장 시작 -->
+
+	<div id="chapter_6">
+		<h2>6장: 라이트 셰프트</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_6.png" alt="Chapter 6 result" title="Chapter 6 result"></p>
+
+		<details>
+			<summary><b>6장 튜토리얼 (펼치기/접기)</b></summary>
+			<p>
+				이제 <b>라이트 셰프트</b>를 완성할 차례입니다.
+			</p>
+
+			<h3>6.1 상수와 컨트롤 변수</h3>
+			<p>
+				그 전에 <b>상수</b>부터 조금 추가해주겠습니다.
+			</p>
+
+			<!-- 6장 상수와 컨트롤 변수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const constants = {									<br />
+						&nbsp; &nbsp; ~										<br />
+						&nbsp; &nbsp; defaultRayLength: 1,					<br />
+						&nbsp; &nbsp; defaultRayColor: new Color(0x444444),	<br />
+						&nbsp; &nbsp; rayLengthMultiplier: 24,				<br />
+						&nbsp; &nbsp; ~										<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 세 개를 넣어주세요.<br />
+				빛 줄기의 길이와 색, 길이 배수입니다.<br />
+				길이 배수는 나중에 설명하도록 하겠습니다.<br />
+				다음은 <b>컨트롤 변수</b>를 추가합시다.
+			</p>
+
+			<!-- 6장 상수와 컨트롤 변수 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						const controls = {										<br />
+						&nbsp; &nbsp; ~											<br />
+						&nbsp; &nbsp; rayLength: constants.defaultRayLength,	<br />
+						&nbsp; &nbsp; rayColor: constants.defaultRayColor,		<br />
+						&nbsp; &nbsp; ~											<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>constants</code>에 있는 값 그대로 넣어주시면 됩니다.
+			</p>
+
+			<h3>6.2 라이트 셰프트 셰이더</h3>
+			<p>
+				제일 중요한 <b>라이트 셰프트 셰이더</b> 작성입니다.<br />
+				일단 이걸 이해하려면 어떻게 빛 줄기를 만드는지 알아야 합니다.<br />
+				먼저 그림부터 보도록 합시다.
+			</p>
+
+			<p><img src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/references/ray_example.png" alt="Ray example" title="Ray example"></p>
+
+			<p>
+				보면 태양으로부터 멀어질수록, 특히 오브젝트에 가려질수록 빛 줄기가 약해집니다.<br />
+				빛 줄기의 강도는 당연히 프래그먼트 셰이더에서 계산해야겠지요?<br />
+				그런데 프래그먼트 셰이더의 호출 단위는 프래그먼트, 즉 화면의 <b>픽셀당 한 번씩 실행</b>된다고 보시면 됩니다.<br />
+				그럼 맨 아래쪽에 빛 줄기가 거의 닿지 않는 나무 부분을 봅시다.<br />
+				이 녀석은 어떻게 자기가 자신보다 위에 있는 나뭇가지에 가려져있다는 사실을 알까요?<br />
+				답은 바로 <b>현재 픽셀로부터 태양까지의 거리 벡터</b>를 이용하는 겁니다.<br />
+				그 다음 구한 거리 벡터를 일정한 양으로 나눠서 그 나눠진 수만큼 각각 가중치 맵에서 가중치를 꺼냅니다.<br />
+				그렇게 꺼낸 각각의 가중치에 점점 커지는 감쇄를 적용한 후, 그 값들을 모두 더해버리면 그게 그 픽셀의 빛 줄기의 양이 됩니다.<br />
+				예...말로 설명하면 확실히 이해하기 난해하죠.<br />
+				그래서 또 그림을 준비해봤습니다.
+			</p>
+
+			<p><img src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/references/light_shaft.png" alt="Light Ray" title="Light Ray"></p>
+
+			<p>
+				자, 여러분이 추출한 <b>가중치 텍스처</b>입니다.<br />
+				<code>stepCount</code>는 방금 말했던 픽셀로부터 태양까지의 나눠진 수를 의미합니다.<br />
+				그리고 그 스텝마다 가중치를 뽑아서 조금씩 늘어나는 감쇄량을 적용한 뒤 누적합니다.<br />
+				슬슬 이해가 되시나요?<br />
+				그럼 이제 셰이더를 작성해봅시다!
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const LightShaftShader = {																								<br />
+						&nbsp; &nbsp; uniforms: {																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'originalRTMap': { value: null },															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'weightRTMap': { value: null },																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'lightPosition': { value: new Vector3() },													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'rayColor': { value: new Color() },															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; 'stepCount': { value: 0 }																	<br />
+						&nbsp; &nbsp; },																										<br />
+						&nbsp; &nbsp;																											<br />
+						&nbsp; &nbsp; vertexShader: `																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; varying vec2 texCoord;																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; void main() {																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; texCoord = uv;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gl_Position = vec4(position, 1.0);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }`,																							<br />
+						&nbsp; &nbsp;																											<br />
+						&nbsp; &nbsp; fragmentShader: `																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; varying vec2 texCoord;																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform sampler2D originalRTMap;															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform sampler2D weightRTMap;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform vec3 lightPosition;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform vec3 rayColor;																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; uniform int stepCount;																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; void main() {																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float initDecay = 0.2;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float distDecay = 0.8;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vec4 albedo = texture2D(originalRTMap, texCoord);								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vec2 dirToLight = lightPosition.xy - texCoord;								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float lengthToLight = length(dirToLight);										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dirToLight /= lengthToLight;													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float deltaLength = min(0.005, lengthToLight * 1.0 / float(stepCount - 1));	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vec2 samplePosition = vec2(0.0, 0.0);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vec2 rayOffset = vec2(0.0, 0.0);												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; vec2 rayDelta = dirToLight * deltaLength;										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float rayIntensity = 0.0;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float stepDecay = distDecay * deltaLength;									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float currentDecay = initDecay;												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; float currentIntensity = 0.0;													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < stepCount; ++i)										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; samplePosition = texCoord + rayOffset;							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentIntensity = texture2D(weightRTMap, samplePosition).x;	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rayOffset += rayDelta;											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rayIntensity += currentIntensity * currentDecay;				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentDecay = clamp(currentDecay - stepDecay, 0.0, 1.0);		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; albedo.rgb = clamp(albedo.rgb + (rayColor * rayIntensity), 0.0, 1.0);			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gl_FragColor = albedo;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }`																							<br />
+						};
+					</td>
+				</tr>
+			</table>
+			<p>
+				꽤 길죠?<br />
+				하나하나 살펴봅시다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						uniforms: {													<br />
+						&nbsp; &nbsp; 'originalRTMap': { value: null },				<br />
+						&nbsp; &nbsp; 'weightRTMap': { value: null },				<br />
+						&nbsp; &nbsp; 'lightPosition': { value: new Vector3() },	<br />
+						&nbsp; &nbsp; 'rayColor': { value: new Color() },			<br />
+						&nbsp; &nbsp; 'stepCount': { value: 0 }						<br />
+						},
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>uniforms</code>의 경우 필요한 변수가 많아졌습니다.<br />
+				<code>origianlRTMap</code>은 계산한 빛 줄기와 <b>실제 렌더된 장면</b>을 합치기 위해 필요합니다.<br />
+				<code>weightRTMap</code>은 빛 줄기의 강도를 계산할 때 사용할 <b>가중치</b> 텍스처입니다.<br />
+				<code>lightPosition</code>은 <b>태양의 UV 공간 위치</b>입니다. 태양과 픽셀 사이의 벡터를 따라 가중치 텍스처를 추출하려면 꼭 필요합니다.<br />
+				<code>rayColor</code>는 <b>빛 줄기의 색상</b>이고요.<br />
+				<code>stepCount</code>는 위에서 말한 벡터의 <b>분할 횟수</b>입니다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-2 -->
+			<table>
+				<tr>
+					<td>
+						vertexShader: `													<br />
+						&nbsp; &nbsp; varying vec2 texCoord;							<br />
+						&nbsp; &nbsp;													<br />
+						&nbsp; &nbsp; void main() {										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; texCoord = uv;						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; gl_Position = vec4(position, 1.0);	<br />
+						&nbsp; &nbsp; }`,
+					</td>
+				</tr>
+			</table>
+			<p>
+				휴, 그나마 버텍스 셰이더는 이전이랑 똑같습니다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-3 -->
+			<table>
+				<tr>
+					<td>
+						fragmentShader: `																						<br />
+						&nbsp; &nbsp; varying vec2 texCoord;																	<br />
+						&nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; uniform sampler2D originalRTMap;															<br />
+						&nbsp; &nbsp; uniform sampler2D weightRTMap;															<br />
+						&nbsp; &nbsp; uniform vec3 lightPosition;																<br />
+						&nbsp; &nbsp; uniform vec3 rayColor;																	<br />
+						&nbsp; &nbsp; uniform int stepCount;																	<br />
+						&nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; void main() {																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float initDecay = 0.2;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float distDecay = 0.8;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; vec4 albedo = texture2D(originalRTMap, texCoord);							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; vec2 dirToLight = lightPosition.xy - texCoord;								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float lengthToLight = length(dirToLight);									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; dirToLight /= lengthToLight;												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float deltaLength = min(0.005, lengthToLight * 1.0 / float(stepCount - 1));	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; vec2 samplePosition = vec2(0.0, 0.0);										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; vec2 rayOffset = vec2(0.0, 0.0);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; vec2 rayDelta = dirToLight * deltaLength;									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float rayIntensity = 0.0;													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float stepDecay = distDecay * deltaLength;									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float currentDecay = initDecay;												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; float currentIntensity = 0.0;												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; for (int i = 0; i < stepCount; ++i)										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; {																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; samplePosition = texCoord + rayOffset;						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentIntensity = texture2D(weightRTMap, samplePosition).x;	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rayOffset += rayDelta;										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rayIntensity += currentIntensity * currentDecay;				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentDecay = clamp(currentDecay - stepDecay, 0.0, 1.0);		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; albedo.rgb = clamp(albedo.rgb + (rayColor * rayIntensity), 0.0, 1.0);		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; gl_FragColor = albedo;														<br />
+						&nbsp; &nbsp; }`
+					</td>
+				</tr>
+			</table>
+			<p>
+				프래그먼트 셰이더가 어려울 수 있어서 주석을 넣었습니다.<br />
+				어려운 부분만 다시 보도록 합시다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-3-1 -->
+			<table>
+				<tr>
+					<td>
+						vec2 dirToLight = lightPosition.xy - texCoord; // 픽셀로부터 태양까지의 벡터	<br />
+						float lengthToLight = length(dirToLight); // 그 벡터의 길이					<br />
+						dirToLight /= lengthToLight; // 길이 구하는 김에 정규화까지
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 픽셀로부터 태양까지의 벡터를 구하는데요.<br />
+				미리 <code>lightPosition</code>을 <b>UV 공간으로 변환</b>시켜서 넣어줬기 때문에 같은 공간에 있는 <code>texCoord</code>와 빼기 연산을 통해 거리 벡터를 구할 수가 있습니다.<br />
+				사실 아직 <code>lightPosition</code>을 변환하는 코드는 작성하지 않았죠? 나중에 작성하면 됩니다.<br />
+				<code>lengthToLight</code> 값을 <code>length()</code> 함수를 통해 구합니다. 이 함수는 GLSL에서 기본으로 제공하는 함수입니다.<br />
+				<code>dirToLight /= lengthToLight</code>는 <code>dirToLight</code> 벡터를 <b>정규화(normalization)</b>합니다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-3-2 -->
+			<table>
+				<tr>
+					<td>
+						float deltaLength = min(0.005, lengthToLight * 1.0 / float(stepCount - 1)); // 벡터를 분할한 길이
+					</td>
+				</tr>
+			</table>
+			<p>
+				다음은 <code>deltaLength</code>인데요.<br />
+				일단 <code>float(stepCount - 1)</code>에서 <code>1</code>을 빼주는 이유가 뭘까요?<br />
+				왜냐하면 추출은 <b>픽셀의 위치부터 시작</b>하기 때문입니다.<br />
+				예를 들어 <code>stepCount</code>가 16일 때, 추출은 16번 반복하겠죠?<br />
+				그럼 원점을 추출하면 반복 수가 <code>15</code>번이 남게 됩니다.<br />
+				여기서 <code>1</code>을 빼지 않은 16으로 벡터를 나누게 되면, 반복당 <code>1/16</code>만큼 앞으로 나아가게 됩니다.<br />
+				그럼 남은 15번의 반복을 끝냈을 때 최종 위치가 <code>16/16</code>이 아닌 <code>15/16</code>이 되어버리고 말아요.<br />
+				따라서 태양의 위치까지 닿으려면 <code>1</code>을 빼주는 것이 맞습니다.<br />
+				그 다음으로는 <code>min()</code> 함수가 보이네요.<br />
+				왜 굳이 <code>0.005</code>보다 크면 <code>0.005</code>로 고정시켜주는 걸까요?<br />
+				왜냐하면 추출 간격이 너무 넓어지면(<code>stepCount</code> 값이 너무 작아지면) <b>빛 줄기의 강도가 퍼지기 때문</b>입니다.<br />
+				그래서 최소한의 간격으로 <code>0.005</code>를 지정해주었습니다.<br />
+				이렇게 하면 태양까지 닿지 않을 수도 있지만, 빛 줄기가 퍼져서 이상한 모습을 보는 일은 없습니다.<br />
+				추가로 <code>float(stepCount - 1)</code> 없이 그냥 <code>0.005</code>로 고정시켜버리면 어떻게 될까요?<br />
+				태양과 너무 가까운 거리에 있는 픽셀도 <code>0.005</code>라는 큰 숫자를 받기 때문에 태양 위치에서 <b>빛이 모이는 듯한 현상</b>이 일어납니다.<br />
+				따라서 <code>min()</code> 함수를 사용한 이유는 태양과의 거리에 따라 <b>적당히 좋은</b> 간격을 지정해주기 위함이라고 생각하시면 편합니다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-3-3 -->
+			<table>
+				<tr>
+					<td>
+						for (int i = 0; i < stepCount; ++i) // stepCount만큼 반복																						<br />
+						{																																			<br />
+						&nbsp; &nbsp; currentIntensity = texture2D(weightRTMap, texCoord + rayOffset).x; // 가중치 텍스처에서 rayOffset만큼 이동한 위치의 가중치를 추출	<br />
+						&nbsp; &nbsp;																																<br />
+						&nbsp; &nbsp; rayOffset += rayDelta; // 다음 지점으로 이동																						<br />
+						&nbsp; &nbsp; rayIntensity += currentIntensity * currentDecay; // 감쇄량 적용 후 누적															<br />
+						&nbsp; &nbsp; currentDecay = clamp(currentDecay - stepDecay, 0.0, 1.0); // 감쇄량 증가														<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				빛 줄기 강도를 계산하는 반복문입니다.<br />
+				주석만 봐도 대충 어떤 식으로 돌아가는지 알 수 있을겁니다.<br />
+				<code>texCoord</code> 값을 수정하면 현재 픽셀이 아닌 다른 픽셀 위치에도 접근할 수 있다는 사실을 기억하세요.<br />
+				마찬가지로 가중치 값은 흑백이기 때문에 RGB 값이 모두 같아서 <code>x</code>값만 가져옵니다.<br />
+				이후엔 다음 지점으로 가고, 누적하고, 감쇄량을 업데이트합니다.<br />
+				여기서 잠깐, 감쇄량의 기본 값을 보면 <code>0.2</code>죠?<br />
+				즉, 계산된 강도의 <code>20%</code>만 누적하겠다는 뜻입니다.<br />
+				분명 가중치도 <code>0.3</code>으로 지정했으니 곱하면 <code>0.06</code>씩 누적되겠군요. 상당히 작습니다.<br />
+				근데 최종 감쇄 증가량을 보면 <code>0.8</code>입니다. <code>0.2</code>보다 큰 이유는 태양과의 거리에 따라 감쇄 추가량이 달라지기 때문입니다.<br />
+				쓰고 보니 조금 복잡한데, 일단은 기본 감쇄량이 <code>0.2</code>이고 최종 감쇄량이 <code>0.8</code>일 때 제일 좋은 결과가 나와서 저는 그렇게 고정했습니다.
+			</p>
+
+			<!-- 6장 라이트 셰프트 셰이더 코드 1-3-4 -->
+			<table>
+				<tr>
+					<td>
+						albedo.rgb = clamp(albedo.rgb + (rayColor * rayIntensity), 0.0, 1.0); // 최종 빛 줄기의 강도에 색상을 곱하고 원래 장면과 병합	<br />
+						gl_FragColor = albedo; // 병합한 값을 최종 출력
+					</td>
+				</tr>
+			</table>
+			<p>
+				마지막으로 빛 줄기 강도와 색상을 곱합니다.<br />
+				그리고 원래 장면에 더하는데요, 곱하지 않고 더하는 이유가 뭘까요?<br />
+				빛 줄기는 원래 장면에 추가되어 씌워지는 값이기 때문입니다.<br />
+				이 경우 값이 <code>1.0</code>을 넘어갈 수 있기 때문에 <code>clamp()</code> 함수로 <code>0~1</code> 범위로 맞춰줍니다.<br />
+				이후 최종 결과 값을 출력하면 셰이더 완성!
+			</p>
+
+			<h3>6.3 패스 생성</h3>
+			<p>
+				이제 <b>패스</b>를 만들어볼까요.<br />
+				먼저 선언부터 해야지요.
+			</p>
+
+			<!-- 6장 패스 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let lightShaftComposer;	<br />
+						let lightShaftPass;
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 두 개를 추가해줍니다.<br />
+				어, <code>weightComposer</code>에 <code>lightShaftPass</code>를 추가해주면 안 될까요?<br />
+				안 됩니다. 컴포저는 <b>하나의 렌더 타겟</b>만 받는 것 같더라고요.<br />
+				가중치 추출할 때 보셨겠지만 렌더 타겟이 <code>weightRT</code>였죠?<br />
+				<code>lightShaftPass</code>는 기본 렌더 타겟에 그릴 것이기 때문에 따로 컴포저를 만들어줘야 합니다.<br />
+				이제 <code>createPasses()</code> 함수로 갑니다.
+			</p>
+
+			<!-- 6장 패스 생성 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createPasses() {																											<br />
+						&nbsp; &nbsp; ~																														<br />
+						&nbsp; &nbsp; lightShaftComposer = new EffectComposer(renderer);																	<br />
+						&nbsp; &nbsp;																														<br />
+						&nbsp; &nbsp; lightShaftPass = new ShaderPass(LightShaftShader);																	<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['originalRTMap'].value = originalRT.texture;													<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['weightRTMap'].value = weightRT.texture;														<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['lightPosition'].value = new Vector3();														<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['rayColor'].value = constants.defaultRayColor;												<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['stepCount'].value = Math.round(constants.defaultRayLength * constants.rayLengthMultiplier);	<br />
+						&nbsp; &nbsp; lightShaftComposer.addPass(lightShaftPass);																			<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>weightPass</code> 만드는 곳 아래에 이렇게 추가해주세요.<br />
+				<code>uniforms</code>의 요소를 넣어주는 걸 보면 <code>lightPosition</code>의 경우 그냥 영벡터를 넣어줍니다.<br />
+				이유는 어차피 <b>매 프레임마다 바꿔줘야하기 때문</b>입니다.<br />
+				태양의 UV 공간 위치는 카메라가 움직일 때, 태양 자체를 움직일 때 등 여러 변수에 의해 바뀔 수 있으니까요.<br />
+				<code>stepCount</code>는 상수에서 정의한 기본 빛 줄기 길이 배수를 곱한 다음 반올림해서 넘겨줍니다. 셰이더에서 사용하는 <code>stepCount</code>는 <code>int</code>형이니까요.<br />
+				다 끝나면 역시 이펙트 컴포저에 추가해줍니다.
+			</p>
+
+			<h3>6.4 태양 위치 좌표계 변환</h3>
+			<p>
+				이제 태양의 위치를 <b>UV 공간</b>으로 옮겨봅시다.<br />
+				그럼 과정은 아래처럼 되겠군요.<br />
+				<b>월드 공간</b> -> <b>뷰 공간</b> -> <b>프로젝션 공간</b> -> <b>UV 공간</b><br />
+				3번이나 변환해줘야 하는 것 같지만 우리에겐 좋은 함수가 하나 있습니다.<br />
+				바로 <code>project()</code> 함수입니다. 인자로 카메라만 넣어주면 카메라의 뷰/프로젝션 행렬을 알아서 곱해줍니다.<br />
+				따라서 이것만 있으면 프로젝션 공간으로는 코드 한 줄로 갈 수 있다는 뜻이죠.<br />
+				코드를 봅시다. <code>update()</code> 함수로 가세요.
+			</p>
+
+			<!-- 6장 태양 위치 좌표계 변환 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function update() {																<br />
+						&nbsp; &nbsp; stats.update();													<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; const uvSunPosition = new Vector3();								<br />
+						&nbsp; &nbsp; uvSunPosition.copy(directionalLight.position);					<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; uvSunPosition.project(camera);									<br />
+						&nbsp; &nbsp; uvSunPosition.x = (uvSunPosition.x + 1) * 0.5;					<br />
+						&nbsp; &nbsp; uvSunPosition.y = (uvSunPosition.y + 1) * 0.5;					<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; lightShaftPass.uniforms['lightPosition'].value = uvSunPosition;	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 추가해주면 됩니다.
+			</p>
+
+			<!-- 6장 태양 위치 좌표계 변환 코드 1-1 -->
+			<table>
+				<tr>
+					<td>
+						uvSunPosition.project(camera);					<br />
+						uvSunPosition.x = (uvSunPosition.x + 1) * 0.5;	<br />
+						uvSunPosition.y = (uvSunPosition.y + 1) * 0.5;
+					</td>
+				</tr>
+			</table>
+			<p>
+				디렉셔널 라이트의 위치를 복사한 <code>uvSunPosition</code> 벡터의 <code>project()</code> 함수를 실행해줍니다.<br />
+				그러면 이제 <code>uvSunPosition</code> 벡터는 프로젝션 공간인 <code>-1~1</code> 범위에 위치하게 됩니다.<br />
+				하지만 <b>UV 공간</b>으로 보내는 과제가 남았습니다. 어떻게 하면 될까요?<br />
+				머릿속으로도 계산 가능할만큼 간단합니다.<br />
+				각 요소에 <code>1</code> 더하고 반으로 나누면 땡입니다. 그럼 알아서 <code>0~1</code> 범위로 맞춰지겠죠?<br />
+				이후 <code>uniforms</code>에 직접 수정해주면 완성입니다.
+			</p>
+
+			<h3>6.5 렌더 순서</h3>
+			<p>
+				그럼 <code>render()</code> 함수도 손 좀 봐야겠죠?
+			</p>
+
+			<!-- 6장 렌더 순서 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function render() {										<br />
+						&nbsp; &nbsp; renderer.setRenderTarget(originalRT);		<br />
+						&nbsp; &nbsp; renderer.render(scene, camera);			<br />
+						&nbsp; &nbsp; renderer.setRenderTarget(null);			<br />
+						&nbsp; &nbsp;											<br />
+						&nbsp; &nbsp; weightComposer.render(scene, camera);		<br />
+						&nbsp; &nbsp; lightShaftComposer.render(scene, camera);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				맨 밑줄에 <code>lightShaftComposer.render(scene, camera);</code>를 추가해주면 됩니다.<br />
+				순서가 굉장히 중요하다는 점 꼭 기억해두시기 바랍니다.<br />
+				<code>weightComposer</code>는 <code>originalRT</code> 깊이 텍스처가 필요하고<br />
+				<code>lightShaftComposer</code>는 <code>originalRT</code>와 <code>weightRT</code>가 모두 필요합니다!
+			</p>
+
+			<h3>6.6 GUI 추가</h3>
+			<p>
+				거의 다 됐습니다. 이제 GUI를 통해 빛 줄기의 색상 및 길이를 수정할 수 있도록 합시다.
+			</p>
+
+			<!-- 6장 GUI 추가 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {																										<br />
+						&nbsp; &nbsp; ~																												<br />
+						&nbsp; &nbsp; lightFolder.add(controls, 'rayLength', 0, 2).listen().onChange(function (value) {								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; lightShaftPass.uniforms['stepCount'].value = Math.round(value * constants.rayLengthMultiplier);	<br />
+						&nbsp; &nbsp; });																											<br />
+						&nbsp; &nbsp; lightFolder.addColor(controls, 'rayColor').listen().onChange(function (value) {								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; lightShaftPass.uniforms['rayColor'].value = value;												<br />
+						&nbsp; &nbsp; });																											<br />
+						&nbsp; &nbsp; ~																												<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 <code>createGUI()</code> 함수에 추가해주세요.<br />
+				의미는 설명 안 해도 알겠죠?
+			</p>
+
+			<h3>6.7 자동 크기 조절</h3>
+			<p>
+				이펙트 컴포저가 늘었으니 이것도 해줘야 합니다.
+			</p>
+
+			<!-- 6장 자동 크기 조절 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function onWindowResize() {														<br />
+						&nbsp; &nbsp; camera.aspect = window.innerWidth / window.innerHeight;			<br />
+						&nbsp; &nbsp; camera.updateProjectionMatrix();									<br />
+						&nbsp; &nbsp;																	<br />
+						&nbsp; &nbsp; renderer.setSize(window.innerWidth, window.innerHeight);			<br />
+						&nbsp; &nbsp; originalRT.setSize(window.innerWidth, window.innerHeight);		<br />
+						&nbsp; &nbsp; weightComposer.setSize(window.innerWidth, window.innerHeight);	<br />
+						&nbsp; &nbsp; lightShaftComposer.setSize(window.innerWidth, window.innHeight);	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				맨 아랫줄에 추가해주세요.<br />
+				휴... 드디어 다 했습니다. 실행하셔도 됩니다.
+			</p>
+			<p>
+				빛 줄기가 잘 보이나요?<br />
+				결과물이 잘 보인다면 제대로 하신겁니다.
+			</p>
+		</details>
+
+		<p><mark>6장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_6.js" title="Light shaft chapter 6 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 7장 시작 -->
+
+	<div id="chapter_7">
+		<h2>7장: 꾸미기</h2>
+		<p><img class="result" src="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/raw/main/tutorial_data/pictures/chapter_results/chapter_7.png" alt="Chapter 7 result" title="Chapter 7 result"></p>
+
+		<details>
+			<summary><b>7장 튜토리얼 (펼치기/접기)</b></summary>
+			<p>
+				자, 7장부터는 옵션입니다.<br />
+				그러니까 해도 되고 안 해도 됩니다.<br />
+				추가할 기능은 다음과 같습니다.
+			</p>
+
+			<ul>
+				<li>태양 또는 카메라를 양 옆으로 왔다갔다하도록 만들기</li>
+				<li>프리셋 기능 만들기</li>
+				<li>태양 오브젝트 변경 기능 만들기</li>
+			</ul>
+
+			<h3>7.1 상수와 컨트롤 변수</h3>
+			<p>
+				<b>상수</b>부터 추가합니다.
+			</p>
+
+			<!-- 7장 상수와 컨트롤 변수 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						const constants = {						<br />
+						&nbsp; &nbsp; ~							<br />
+						&nbsp; &nbsp; autoSunMoveSpeed: 0.5,	<br />
+						&nbsp; &nbsp; autoCameraMoveSpeed: 0.3,	<br />
+						&nbsp; &nbsp; ~							<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 두 개 넣어주시면 됩니다.<br />
+				각각 태양과 카메라의 자동 이동 속도입니다.<br />
+				다음은 <b>컨트롤 변수</b>입니다.
+			</p>
+
+			<!-- 7장 상수와 컨트롤 변수 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						const controls = {						<br />
+						&nbsp; &nbsp; ~							<br />
+						&nbsp; &nbsp; presets: 'default',		<br />
+						&nbsp; &nbsp; sunObject: 'Sphere',		<br />
+						&nbsp; &nbsp; autoSunMove: false,		<br />
+						&nbsp; &nbsp; autoCameraMove: false,	<br />
+						&nbsp; &nbsp; ~							<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 추가해주세요.<br />
+				<code>presets</code>와 <code>sunObject</code>는 목록용이고, <code>autoSunMove</code>와 <code>autoCameraMove</code>는 체크박스용입니다.
+			</p>
+
+			<h3>7.2 움직임 GUI 추가</h3>
+			<p>
+				<b>체크박스</b>를 만들어봅시다.<br />
+				바꿔줄 게 딱히 없어서 코드가 매우 짧아요.
+			</p>
+
+			<!-- 7장 움직임 GUI 추가 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {													<br />
+						&nbsp; &nbsp; ~															<br />
+						&nbsp; &nbsp; generalFolder.add(controls, 'autoSunMove').listen();		<br />
+						&nbsp; &nbsp; generalFolder.add(controls, 'autoCameraMove').listen();	<br />
+						&nbsp; &nbsp; ~															<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 두 줄만 넣어주시면 됩니다.<br />
+				<code>onChange()</code> 함수도 필요 없어요.
+			</p>
+
+			<h3>7.3 움직임 로직 구현</h3>
+			<p>
+				자, 이제 <code>update()</code> 함수에 로직을 넣어봅시다.<br />
+				왔다갔다 하는 데 가장 좋은 함수는 역시 <code>sin()</code> 함수겠죠?<br />
+				그럼 안에 넣을 누적 각도 값이 필요합니다.<br />
+				누적 없이 그냥 넣으면 컴퓨터 성능에 따라 이동 속도가 천차만별이 되어버려요.<br />
+				그럼 컴퓨터 사양에 관계없이 일정한 것은 무엇일까요? 바로 시간입니다.<br />
+				<code>const clock = new THREE.Clock();</code>
+				<code>run()</code> 함수 위에 이렇게 추가해주세요.<br />
+				이게 있으면 이제 시간을 잴 수 있습니다.
+			</p>
+
+			<!-- 7장 움직임 로직 구현 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let sunTime = 0;	<br />
+						let cameraTime = 0;
+					</td>
+				</tr>
+			</table>
+			<p>
+				누적 시간 변수도 추가해주시고요.<br />
+				이제 <code>update()</code> 함수로 갑니다.
+			</p>
+
+			<!-- 7장 움직임 로직 구현 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function update() {																								<br />
+						&nbsp; &nbsp; ~																									<br />
+						&nbsp; &nbsp; const deltaTime = clock.getDelta();																<br />
+						&nbsp; &nbsp;																									<br />
+						&nbsp; &nbsp; if (controls.autoSunMove) {																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunObject.position.x = Math.sin(sunTime) * 500;										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; directionalLight.position.x = sunObject.position.x;									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunTime += deltaTime;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; constants.autoSunMoveSpeed;															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunTime = (sunTime >= Math.PI * 2) ? sunTime - Math.PI * 2 : sunTime;				<br />
+						&nbsp; &nbsp; }																									<br />
+						&nbsp; &nbsp; if (controls.autoCameraMove) {																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; camera.position.x = Math.sin(cameraTime) * 300;										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; cameraTime += deltaTime;															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; constants.autoCameraMoveSpeed;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; cameraTime = (cameraTime >= Math.PI * 2) ? cameraTime - Math.PI * 2 : cameraTime;	<br />
+						&nbsp; &nbsp; }																									<br />
+						&nbsp; &nbsp; ~																									<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이렇게 추가해주세요. 하나씩 봅시다.
+			</p>
+
+			<!-- 7장 움직임 로직 구현 코드 2-1 -->
+			<table>
+				<tr>
+					<td>
+						const deltaTime = clock.getDelta();
+					</td>
+				</tr>
+			</table>
+			<p>
+				먼저 보이는 건 이거군요. <code>getDelta()</code> 함수가 뭘까요?<br />
+				<code>getDelta()</code> 함수는 마지막으로 <code>getDelta()</code> 함수가 실행됐을 때부터 방금 호출한 때까지의 <b>걸린 시간</b>을 반환합니다.<br />
+				즉, 지금 이 친구가 반환하는 값은 이전 프레임으로부터 지금 프레임까지의 걸린 시간이 되겠군요!<br />
+				이걸 왜 쓸까요? 이게 있어야 <b>컴퓨터 사양에 관계없이 동일한 속도</b>로 오브젝트를 움직일 수 있기 때문입니다.<br />
+				예를 들어 1초 동안 <code>getDelta()</code> 함수의 값을 누적했다고 합시다.<br />
+				A의 컴퓨터는 그 1초 동안 60프레임이, B의 컴퓨터는 30프레임이 나왔습니다.<br />
+				하지만 누적 값은 같습니다. A는 1/60 값이 60번 더해졌을 것이고, B는 1/30 값이 30번 더해졌을 테니까요.<br />
+				지금은 이해가 안 간다고 해도 직접 쓰는 걸 보면 이해가 갈 겁니다.
+			</p>
+
+			<!-- 7장 움직임 로직 구현 코드 2-2 -->
+			<table>
+				<tr>
+					<td>
+						if (controls.autoSunMove) {															<br />
+						&nbsp; &nbsp; sunObject.position.x = Math.sin(sunTime) * 500;						<br />
+						&nbsp; &nbsp; directionalLight.position.x = sunObject.position.x;					<br />
+						&nbsp; &nbsp; controls.sunPositionX = sunObject.position.x;							<br />
+						&nbsp; &nbsp;																		<br />
+						&nbsp; &nbsp; sunTime += deltaTime;													<br />
+						&nbsp; &nbsp; constants.autoSunMoveSpeed;											<br />
+						&nbsp; &nbsp; sunTime = (sunTime >= Math.PI * 2) ? sunTime - Math.PI * 2 : sunTime;	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>autoSunMove</code>가 활성화되었을 때만 작동합니다.<br />
+				그리고 <code>x</code>위치 값을<code>Math.sin(sunTime)</code>에<code>500</code>을 곱해서 설정합니다.<br />
+				이러면 이제 <code>x</code>위치 값은<code>-500~500</code> 범위를 왔다갔다 하겠죠?<br />
+				이후엔 <code>sunTime</code>에<code>deltaTime</code>을 더합니다. 추가로 속도 값을 곱해서요.<br />
+				이러면 <code>sunTime</code>값은 실제 시간 1초가 지났을 때 무조건<code>1</code>이 됩니다. 10초가 지나면<code>10</code>이 되고요.<br />
+				하지만 라디안은 <code>PI * 2</code>부터 다시 원점으로 돌아오죠?<br />
+				그래서 삼항 연산자를 사용해 <code>0~(PI * 2)</code> 범위에 있도록 만들어줬습니다.
+			</p>
+
+			<!-- 7장 움직임 로직 구현 코드 2-3 -->
+			<table>
+				<tr>
+					<td>
+						if (controls.autoCameraMove) {																	<br />
+						&nbsp; &nbsp; camera.position.x = Math.sin(cameraTime) * 300;									<br />
+						&nbsp; &nbsp;																					<br />
+						&nbsp; &nbsp; cameraTime += deltaTime;															<br />
+						&nbsp; &nbsp; constants.autoCameraMoveSpeed;													<br />
+						&nbsp; &nbsp; cameraTime = (cameraTime >= Math.PI * 2) ? cameraTime - Math.PI * 2 : cameraTime;	<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				카메라의 움직임도 똑같습니다.<br />
+				범위랑 속도만 조금 다를 뿐입니다.
+			</p>
+			<p>
+				이제 실행해보세요.<br />
+				체크박스를 조작하면 태양과 카메라가 잘 움직이나요?
+			</p>
+
+			<h3>7.4 프리셋 GUI 추가</h3>
+			<p>
+				이제 프리셋 기능을 만들어봅시다.<br />
+				먼저 변수 하나를 넣어줘야해요.
+			</p>
+
+			<!-- 7장 프리셋 GUI 추가 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						let fantasyTime = 0;
+					</td>
+				</tr>
+			</table>
+			<p>
+				<code>fantasy</code> 프리셋을 위한 애니메이션 변수입니다.<br />
+				이제 <code>createGUI()</code> 함수로 갑시다.
+			</p>
+
+			<!-- 7장 프리셋 GUI 추가 코드 2 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {																														<br />
+						&nbsp; &nbsp; ~																																<br />
+						&nbsp; &nbsp; function changePreset(																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; backgroundColor,																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; ambientLightColor,																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunLightColor,																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunLightIntensity,																								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunColor,																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunPositionY,																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; rayLength,																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; rayColor) {																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.backgroundColor = backgroundColor;																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.ambientLightColor = ambientLightColor;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.sunLightColor = sunLightColor;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.sunLightIntensity = sunLightIntensity;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.sunColor = sunColor;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionY = sunPositionY;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.rayLength = rayLength;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.rayColor = rayColor;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; renderer.setClearColor(controls.backgroundColor);																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; ambientLight.color = controls.ambientLightColor;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; directionalLight.color = controls.sunLightColor;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; directionalLight.intensity = controls.sunLightIntensity;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunObject.material.color = controls.sunColor;																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; directionalLight.position.y = sunObject.position.y = controls.sunPositionY;										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; lightShaftPass.uniforms['stepCount'].value = Math.round(controls.rayLength * constants.rayLengthMultiplier);	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; lightShaftPass.uniforms['rayColor'].value = rayColor;															<br />
+						&nbsp; &nbsp; }																																<br />
+						&nbsp; &nbsp; ~																																<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				조금은 무식해보이는 <code>changePreset()</code> 함수를 <code>createGUI()</code> 함수 안에 추가해줍니다.<br />
+				프리셋에서 변경하는 값의 리스트를 인자로 보내면 실제로 그렇게 적용시켜주는 함수입니다.
+			</p>
+
+			<!-- 7장 프리셋 GUI 추가 코드 3 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {																																			<br />
+						&nbsp; &nbsp; ~																																					<br />
+						&nbsp; &nbsp; generalFolder.add(controls, 'presets', ['default', 'afternoon', 'sunset', 'night', 'eclipse', 'fantasy']).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; switch (value) {																													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'default':																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultBackgroundColor,															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultAmbientLightColor,														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultDirectionalLightColor, 1,												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultSunColor,																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.maxSunDistance,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 1, constants.defaultRayColor);															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'afternoon':																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x40829c),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x51411f),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x948161), 1,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0xe1f8fe),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.maxSunDistance,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 1.5, new Color(0x3e6a8e));																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'sunset':																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0xc39737),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x3e3418),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x3e3418), 1,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0xff9242),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 50,																						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 2, new Color(0x873636));																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'night':																											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x252837),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x201f2e),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x2c2d44), 0.4,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x8282c7),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.maxSunDistance,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 0.8, new Color(0x111122));																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'eclipse':																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x140505),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x773131),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x202010), 1,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x222222),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.maxSunDistance,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 2, new Color(0x460c0c));																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'fantasy':																										<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; changePreset(																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x874089),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x40387a),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x9a4747), 1,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; new Color(0x874089),																		<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.maxSunDistance,																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 2, new Color(0x557799));																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }																																	<br />
+						&nbsp; &nbsp; });																																				<br />
+						&nbsp; &nbsp; ~																																					<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				이제 GUI 항목을 각각 추가해주세요.<br />
+				많죠? 제가 다 하나하나 정한 값들입니다.<br />
+				이걸 보면 <code>changePreset()</code> 함수 만들길 잘했다는 생각이 들 겁니다.
+			</p>
+
+			<h3>7.5 프리셋 로직 구현</h3>
+			<p>
+				이제 <code>update()</code> 함수로 가서 <code>fantasy</code> 프리셋의 <b>색상 변화 애니메이션</b>을 넣어봅시다.
+			</p>
+
+			<!-- 7장 프리셋 로직 구현 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function update() {																									<br />
+						&nbsp; &nbsp; ~																										<br />
+						&nbsp; &nbsp; if (controls.presets == 'fantasy') {																	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; const color = new Color(0x874089);														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; const wave = (Math.cos(fantasyTime) + 1) * 0.5;											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; color.r = THREE.MathUtils.clamp(color.r * wave, 0, 1);									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; color.g = THREE.MathUtils.clamp(color.g * wave, 0, 1);									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; color.b = THREE.MathUtils.clamp(color.b * wave, 0, 1);									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.backgroundColor = color;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; controls.sunColor = color;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; renderer.setClearColor(color, 1);														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunObject.material.color = color;														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; fantasyTime += deltaTime;																<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; fantasyTime = (fantasyTime >= Math.PI * 2) ? fantasyTime - Math.PI * 2 : fantasyTime;	<br />
+						&nbsp; &nbsp; }																										<br />
+						&nbsp; &nbsp; ~																										<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				태양 움직이는 로직하고 약간 비슷하면서 다릅니다.<br />
+				<code>cos()</code> 함수의 결과에 <code>1</code>을 더하는군요. 그럼 범위가 <code>0~2</code>가 되겠죠?<br />
+				이걸 반으로 나누면 다시 <code>0~1</code> 범위로 좁혀집니다.<br />
+				색상의 경우 <b>음수로 떨어질 수 없기 때문</b>에 이렇게 한 거예요.<br />
+				그렇게 나온 값을 색상의 각 요소에 곱하면 끝입니다.<br />
+				나머지는 움직임 로직과 같아요. 다만 속도를 지정하지 않아서 <code>PI * 2</code>초마다 한 싸이클이 반복됩니다.
+			</p>
+			<p>
+				이제 실행해보세요. 잘 되나요?
+			</p>
+
+			<h3>7.6 태양 오브젝트 GUI 추가</h3>
+			<p>
+				마지막으로 태양 오브젝트를 바꿔보겠습니다!<br />
+				<code>createGUI()</code> 함수에 이것만 추가해주시면 됩니다.
+			</p>
+
+			<!-- 7장 태양 오브젝트 GUI 추가 코드 1 -->
+			<table>
+				<tr>
+					<td>
+						function createGUI() {																													<br />
+						&nbsp; &nbsp; ~																															<br />
+						&nbsp; &nbsp; generalFolder.add(controls, 'sunObject', ['sphere', 'sun_draw', 'cat_draw', 'none']).listen().onChange(function (value) {	<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; const textureLoader = new THREE.TextureLoader();															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; let planeGeometry;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; let planeMaterial;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; let planeTexture;																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; scene.remove(sunObject);																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; switch (value) {																							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'sphere':																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject = new THREE.Mesh(														<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultSunObjectGeometry,								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; constants.defaultSunObjectMaterial);								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject.position.set(															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionX,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionY,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionZ);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject.scale.multiplyScalar(40);												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; scene.add(sunObject);															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'sun_draw':																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeTexture = textureLoader.load('sprites/sun.png');							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeGeometry = new THREE.PlaneGeometry(90, 90);								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeMaterial = new THREE.MeshBasicMaterial({									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; map: planeTexture,												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alphaTest: 0.05													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;	&nbsp; &nbsp; &nbsp; &nbsp;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject = new THREE.Mesh(planeGeometry, planeMaterial);						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject.position.set(															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionX,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionY,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionZ);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; scene.add(sunObject);															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; case 'cat_draw':																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeTexture = textureLoader.load('sprites/cat.png');							<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeGeometry = new THREE.PlaneGeometry(90, 90);								<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; planeMaterial = new THREE.MeshBasicMaterial({									<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; map: planeTexture,												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alphaTest: 0.05													<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });																				<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;	&nbsp; &nbsp; &nbsp; &nbsp;																					<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject = new THREE.Mesh(planeGeometry, planeMaterial);						<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sunObject.position.set(															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionX,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionY,											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; controls.sunPositionZ);											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; scene.add(sunObject);															<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;																			<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; }																											<br />
+						&nbsp; &nbsp; &nbsp; &nbsp;																												<br />
+						&nbsp; &nbsp; &nbsp; &nbsp; sunObject.material.color = controls.sunColor;																<br />
+						&nbsp; &nbsp; });																														<br />
+						&nbsp; &nbsp; ~																															<br />
+						}
+					</td>
+				</tr>
+			</table>
+			<p>
+				저는 <code>직접 그린 태양</code>과 <code>직접 그린 고양이</code>로 정했습니다.<br />
+				재질에 텍스처를 적용하는 내용은 수업에서 배운 텍스처 적용과 동일하므로 아실 거라 믿습니다.<br />
+				아, <code>alphaTest</code> 값은 알파 클립 값을 나타냅니다. 즉, 저 값보다 작은 알파 값은 투명한 것으로 처리합니다.<br />
+				저걸 설정해주지 않으면 투명도가 없는 것으로 처리되니 꼭 넣어주세요!<br />
+				이걸로 꾸미기까지 모두 완성입니다.
+			</p>
+			<p>
+				결과물이 잘 보인다면 제대로 하신겁니다.<br />
+				끝까지 수고 많았습니다!
+			</p>
+		</details>
+
+		<p><mark>7장의 전체 스크립트는 <a href="https://git.ajou.ac.kr/shh1473/cg-tutorial/-/blob/main/tutorial_data/scripts/script_7.js" title="Light shaft chapter 7 source code"><b>여기</b></a>에 있습니다.</mark></p>
+	</div>
+
+	<hr /> <!-- 파트 분리선: 여기서 부록 시작 -->
+
+	<div id="chapter_8">
+		<h2>8장: 소스 & 레퍼런스</h2>
+
+		<h3>소스</h3>
+		<ul>
+			<li><a href="https://github.com/mrdoob/three.js/blob/master/examples/webgl_depth_texture.html">Three.js webgl_depth_texture</a></li>
+			<li><a href="https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing.html">Three.js webgl_postprocessing</a></li>
+			<li><a href="https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing_unreal_bloom.html">Three.js webgl_postprocessing_unreal_bloom</a></li>
+			<li><a href="https://github.com/mrdoob/three.js/blob/master/examples/webgl_postprocessing_godrays.html">Three.js webgl_postprocessing_godrays</a></li>
+		</ul>
+
+		<h3>레퍼런스</h3>
+		<ul>
+			<li>Pope Kim. 셰이더 프로그래밍 입문. 한빛미디어, 2012</li>
+			<li>Feinstein, Doron. HLSL Development Cookbook. Packt, 2013</li>
+			<li>Dirksen, Jos. Learning Three.js - the JavaScript 3D Library for WebGL (Second Edition). Packt, 2015</li>
+		</ul>
+	</div>
 </body>
 </html>
\ No newline at end of file
diff --git a/models/tree.obj b/tutorial_data/scripts/models/tree.obj
similarity index 100%
rename from models/tree.obj
rename to tutorial_data/scripts/models/tree.obj
diff --git a/sprites/cat.png b/tutorial_data/scripts/sprites/cat.png
similarity index 100%
rename from sprites/cat.png
rename to tutorial_data/scripts/sprites/cat.png
diff --git a/sprites/sun.png b/tutorial_data/scripts/sprites/sun.png
similarity index 100%
rename from sprites/sun.png
rename to tutorial_data/scripts/sprites/sun.png