-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCODE_WALKTHROUGH.html
More file actions
368 lines (318 loc) · 17.4 KB
/
Copy pathCODE_WALKTHROUGH.html
File metadata and controls
368 lines (318 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>코드 정독 — 채색화 숲 버전</title>
<style>
:root{
--bg:#0b0e0c; --panel:#0f130d; --line:#222a1e; --line2:#2c3a28;
--fg:#e3e9e0; --dim:#8b988380; --accent:#9fe24a; --blue:#5aa6ff; --warn:#ffb45c; --pink:#e58fb0;
--mono:"SF Mono",ui-monospace,"JetBrains Mono",Menlo,Consolas,monospace;
--sans:"Helvetica Neue",Helvetica,Arial,"Apple SD Gothic Neo","Malgun Gothic",sans-serif;
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);scroll-behavior:smooth}
body{display:grid;grid-template-columns:296px 1fr;min-height:100vh}
aside{position:sticky;top:0;height:100vh;overflow-y:auto;border-right:1px solid var(--line2);background:var(--panel);padding:24px 18px}
aside .brand{font-family:var(--mono);font-size:12px;letter-spacing:.16em;color:var(--accent);text-transform:uppercase;margin-bottom:4px}
aside .sub{font-size:12px;color:#8b9883;margin-bottom:20px;line-height:1.5}
nav a{display:block;font-size:12.5px;color:#8b9883;text-decoration:none;padding:5px 10px;border-radius:5px;line-height:1.4;border-left:2px solid transparent;transition:.15s}
nav a:hover{color:var(--fg);background:rgba(255,255,255,.04)}
nav a.h3{padding-left:24px;font-size:11.5px}
nav a.active{color:var(--accent);border-left-color:var(--accent);background:rgba(159,226,74,.07)}
main{min-width:0;padding:46px 6vw 120px;max-width:1020px}
.doc{line-height:1.72;font-size:15.5px}
.doc h1{font-size:clamp(28px,3.6vw,46px);line-height:1.1;letter-spacing:-.02em;margin:.2em 0 .5em}
.doc h2{font-size:clamp(21px,2.4vw,30px);margin:2.4em 0 .6em;padding-top:.6em;border-top:1px solid var(--line2)}
.doc h3{font-size:clamp(16px,1.7vw,20px);margin:1.6em 0 .4em;color:#fff}
.doc h4{font-size:15px;margin:1.3em 0 .3em;color:var(--blue)}
.doc p{margin:.7em 0;color:#cdd6c8}
.doc a{color:var(--accent);text-decoration:none}
.doc strong,.doc b{color:#fff}
.doc ul,.doc ol{margin:.6em 0;padding-left:1.3em}
.doc li{margin:.3em 0;color:#cdd6c8}
.doc blockquote{margin:1em 0;padding:10px 16px;border-left:3px solid var(--accent);background:rgba(159,226,74,.06);border-radius:0 6px 6px 0;color:#c4cebb}
.doc blockquote p{margin:.3em 0}
.doc :not(pre)>code{font-family:var(--mono);font-size:.88em;background:#1a2014;color:#bfe27a;padding:2px 6px;border-radius:4px;border:1px solid #243018}
.doc pre{background:#0a0d08;border:1px solid var(--line2);border-radius:8px;padding:14px 16px;overflow:auto;margin:1em 0}
.doc pre code{font-family:var(--mono);font-size:13px;line-height:1.62;color:#dbe3d4;background:none;border:none;padding:0;white-space:pre}
.doc table{border-collapse:collapse;width:100%;margin:1.2em 0;font-size:14px;display:block;overflow-x:auto}
.doc th,.doc td{border:1px solid var(--line2);padding:8px 12px;text-align:left;vertical-align:top}
.doc th{background:#141a10;color:#fff;font-weight:700;white-space:nowrap}
.doc tr:nth-child(even) td{background:rgba(255,255,255,.015)}
.doc hr{border:none;border-top:1px solid var(--line2);margin:2.4em 0}
.top{font-family:var(--mono);font-size:11px;color:#8b9883;margin-bottom:8px;letter-spacing:.06em}
.loading{color:#8b9883;font-family:var(--mono);padding:40px 0}
@media(max-width:880px){body{grid-template-columns:1fr}aside{display:none}main{padding:32px 22px 100px}}
</style>
</head>
<body>
<aside>
<div class="brand">Code Walkthrough</div>
<div class="sub">채색화 숲 버전 · 현재 코드<br>파일별 · 라인별 상세</div>
<nav id="toc"></nav>
</aside>
<main>
<div class="top">replica/CODE_WALKTHROUGH.html · 동일한 .md에서 렌더링</div>
<div class="doc" id="content"><div class="loading">문서를 불러오는 중…</div></div>
</main>
<script type="text/markdown" id="src">
# 코드 정독 (Code Walkthrough) — 채색화 숲 버전
> `replica/`의 **현재 코드**가 어떻게 동작하는지 파일별·라인별로 설명합니다.
> 이 프로젝트는 "안개 낀 3D 산 복제" → "절차적 채색화 숲"으로 발전했고, 이 문서는 **현재(숲) 상태** 기준입니다.
> 처음이면 10장(GLSL 미니 사전)을 먼저 훑고 오면 셰이더가 잘 읽힙니다.
## 0. 큰 그림
### 파일 맵
```
src/
main.js ← 진입점: 렌더러·카메라·루프·패널, 모든 모듈 조립 + applyPreset(메뉴 전환)
Terrain.js ← 산 (높이맵 변위 + 숲/암석/폭포/조명/안개) ★ 셰이더의 심장
Sky.js ← 하늘 그라데이션 돔
Water.js ← 물 (지형 높이 읽어 해안선·반사·잔물결)
Fluid.js ← GPU 유체 시뮬 (마우스 안개), 8종 셰이더 ping-pong
Post.js ← 후처리: 유체 합성 + 블룸 + 톤매핑
projects.js ← 데이터: 11개 프로젝트 프리셋 (원본 bundle.js에서 추출한 실값)
ui.js ← 메뉴 리스트 + 클릭 핸들 + 로더
panel.js ← 13겹 빌드스택 토글 패널
```
### 데이터 흐름
```
높이맵·텍스처 ─▶ Terrain(정점 변위 + 표면 셰이더) ─┐
Sky · Water ─────────────────┤─▶ RenderPass(텍스처로 렌더)
마우스 ─▶ Fluid(유체) ─▶ dye/velocity ────────────┘ │
▼
Composite(유체 덧칠) ▶ Bloom ▶ Tonemap ▶ 화면
메뉴 클릭 ─▶ applyPreset() ─▶ GSAP이 uniforms를 1.6초 트윈 ─▶ 다음 프레임부터 산·색 변함
```
핵심 통찰: **거의 모든 효과는 "uniform 하나"로 제어**됩니다. 13겹 패널·메뉴 전환이 전부 uniform 값을 바꾸는 것이고, 셰이더가 매 프레임 그 값으로 다시 그립니다.
## 1. 한 프레임에 일어나는 일
`main.js`의 `frame()`이 초당 약 60번:
```
1. fluid.update(dt) // 유체 물리 1스텝 (Fluid 켜졌을 때)
2. terrain.update(t) // uTime 갱신 (living motion·폭포 애니용)
3. water.update(t, cam) // uTime + 카메라 위치 (반사용)
4. controls.update() // OrbitControls 카메라
5. post.render() // 씬→텍스처→합성→블룸→톤매핑→화면
```
**메뉴 전환은 루프 밖**: 클릭 → `applyPreset()` → GSAP이 uniform을 트윈. 루프는 매 프레임 "현재 uniform 값"으로 그림.
## 2. 반드시 알 개념 4가지
### (A) 셰이더 = GPU에서 점/픽셀마다 도는 함수
- **버텍스 셰이더**: 정점(14.8만)마다 1번 — 점의 최종 위치
- **프래그먼트 셰이더**: 픽셀(200만)마다 1번 — 그 픽셀 색
- `varying` = 버텍스→프래그 값 전달(보간), `uniform` = JS→셰이더 상수
### (B) 텍스처 = "숫자 격자"
- `texture2D(t, uv)` = uv 위치의 RGBA(0~1). 높이맵은 `.r`을 **높이**로 사용.
### (C) plane 회전 규칙
- `PlaneGeometry`는 XY 평면. `mesh.rotation.x = -PI/2`로 눕히면 **로컬 +Z(변위) = 월드 +Y(위)**. 그래서 `pos.z += 높이` → 산이 위로.
### (D) 두 상태 모프 (two-state cross-fade)
- 산 전환 시 **출발(A)·도착(B) 지형을 둘 다 완성**해두고 `uMorph`(0→1)로 높이 보간.
- 이유: 높이맵 offset을 연속으로 밀면 텍스처가 **옆으로 미끄러져** 보임. 두 완성본을 cross-fade하면 봉우리가 **제자리에서** 솟고 꺼짐.
## 3. `main.js`
### 렌더러 / 톤매핑
```js
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 밝기 1.0 초과를 압축(흰색 폭발 방지)
renderer.toneMappingExposure = 0.95;
```
### 카메라 (원본 실값)
```js
new THREE.PerspectiveCamera(30, aspect, 0.1, 3000); // fov 30
camera.position.set(0, 65, 200);
camera.lookAt(0, 0, 0);
```
### 모듈 생성
```js
const terrain = new Terrain(manager); scene.add(terrain.mesh);
const sky = new Sky(); scene.add(sky.mesh);
const water = new Water(terrain); scene.add(water.mesh); // terrain의 높이 uniform 공유
const fluid = new Fluid(renderer, { simRes: 256 }); // 오프스크린
const post = new Post(renderer, scene, camera);
```
### 메뉴 전환 = `applyPreset()` (핵심)
클릭하면 그 프로젝트로 전환. **두 상태 모프** + **분위기 트윈** + **물 높이**:
```js
function applyPreset(p) {
// 1) 현재 "도착(B)"을 "출발(A)"로 스냅샷
U.uDispAlphaA.value.copy(U.uDispAlphaB.value);
U.uDispScaleA.value = U.uDispScaleB.value; ...
// 2) 새 프리셋을 "도착(B)"으로 (즉시)
U.uDispAlphaB.value.set(...one-hot p.disp...);
U.uDispScaleB.value = p.dispScale * DISP_MULT; // DISP_MULT=1.6
...
// 3) uMorph 0→1 트윈 → 제자리 모프
U.uMorph.value = 0;
gsap.to(U.uMorph, { value: 1, duration: 1.6 });
// 4) 분위기: 프로젝트 fog색을 크림/초록과 섞어 하늘·물·안개·배경에 틴트
const fog = new THREE.Color(p.fog);
const haze = fog.clone().lerp(new THREE.Color('#e6e3d4'), 0.62);
tweenColorTo(U.uFogColor.value, haze);
tweenColorTo(sky.uniforms.uHorizon.value, haze); ...
// 5) 해수면 = 산 높이의 55% (원본: 봉우리가 물 위 섬처럼)
const waterY = terrain.mesh.position.y + 0.55 * (p.dispScale * DISP_MULT);
gsap.to(water.mesh.position, { y: waterY, duration: 1.6 });
gsap.to(water.uniforms.uWaterLevel, { value: waterY, duration: 1.6 });
}
```
### 메뉴는 **클릭** 선택 (호버 아님)
`ui.js`가 각 `<a>`에 `click` → `applyPreset(PROJECTS[i])`. 다른 메뉴 클릭 전까지 유지.
### 빌드스택 패널 (13겹)
```js
new Panel([
{ id:1, label:'Terrain', ... }, { id:2, label:'Color grade', ... uGradeOn },
{ id:3, label:'Lighting', ... uLightOn }, { id:4, label:'Fog / haze', ... uFogOn },
{ id:5, label:'Sky', ... }, { id:6, label:'Water', ... },
{ id:7, label:'Living motion', ... uAnimOn }, { id:8, label:'Click morph', ... state.morph },
{ id:9, label:'Fluid mist', ... 유체+합성 함께 }, { id:10, label:'Bloom', ... },
{ id:11, label:'Tonemap', ... }, { id:12, label:'Rock / Waterfall', ... uRockOn },
{ id:13, label:'Forest', ... uForestOn },
]);
panel.reset(); // 시작은 Terrain만
```
## 4. `Terrain.js` — 가장 중요한 파일
### 4.1 버텍스 — 산 모양 + 법선 + 곡률
**두 상태 높이 블렌드** (모프):
```glsl
float bh(vec2 uv, vec4 alpha, vec2 off) { // 4개 높이맵을 알파로 섞음
return (texture2D(tDisp0, uv+off).r*alpha.x + ... ) / wsum;
}
float worldH(vec2 uv) { // 두 완성 지형을 uMorph로
return mix(bh(uv, uDispAlphaA, uDispOffA) * uDispScaleA,
bh(uv, uDispAlphaB, uDispOffB) * uDispScaleB, uMorph);
}
```
**main()**:
```glsl
float h = worldH(uv);
h += (texture2D(tDisp0, uv*1.7 + uTime*0.01).r - 0.5) * 0.8 * uAnimOn; // living motion(미세)
vHeight = mix(bh(uv,A,offA), bh(uv,B,offB), uMorph); // 0~1 색용 높이
// 법선 = 이웃 높이차(기울기)
vNormal = normalize(vec3(-(hX-h)/dx, 1.0, -(hY-h)/dy));
// 곡률(Laplacian): 이웃합 - 4*중심 → 양수=오목한 골(폭포용)
vConcavity = (hX + hXn + hY + hYn - 4.0*h);
pos.z += h; // 위로 변위
```
### 4.2 프래그먼트 — 표면 색
**A) 미스티 마블** (Forest OFF):
```glsl
vec3 marble = mix(vec3(0.5), mix(uColorA, uColorB, t), uGradeOn) * (0.4 + gg*0.75);
```
**B) 절차적 숲** (Forest ON) — Voronoi 나무:
```glsl
vec2 fuv = vUv * 240.0; // 칸 밀도
for (y=-1..1) for (x=-1..1) { 점=hash2(셀+이웃); md=min(md, 거리²); } // 가장 가까운 셀
float dab = smoothstep(sz, sz*0.35, sqrt(md)); // 둥근 나무 도장
float slope = 1.0 - normalize(vNormal).y; // 경사
float steepM = smoothstep(0.40, 0.54, slope);
dab *= 1.0 - steepM * uRockOn; // 절벽엔 나무 제거(암석 자리)
vec3 baseGreen = mix(진초록, 연두, t); // 높이로 초록 그라데이션
vec3 forestAlbedo = baseGreen * mix(0.74,1.0,dab) * (0.90+0.2*r2);
if (r3>0.93) forestAlbedo = mix(forestAlbedo, 핑크, dab*0.6); // 벚꽃
```
**암석 + 폭포** (uRockOn):
```glsl
vec3 rw = mix(forestAlbedo, 회청색바위, steepM); // 가파른 곳 바위
float valley = smoothstep(0.15, 1.0, vConcavity); // 오목한 골만
float wf = valley * 세로줄기 * steepM * 높이대 * 흐름 * uRockOn; // 폭포 마스크
forestAlbedo = mix(forestAlbedo, rw, uRockOn);
```
**표면 선택 + 조명 + 폭포 + 안개**:
```glsl
vec3 albedo = mix(marble, forestAlbedo, uForestOn); // 13번 토글
float diff = dot(normalize(vNormal), uSunDir);
vec3 lit = albedo*(0.25 + 0.8*diff) + 앰비언트 + 하이라이트;
vec3 col = mix(albedo, lit, uLightOn);
col = mix(col, vec3(0.20,0.55,1.0), wf*uForestOn); // 파란 폭포(조명 뒤)
col = mix(col, uFogColor, smoothstep(uFogNear,uFogFar,vViewZ)*uFogOn); // 대기 원근
```
### 4.3 JS
- 높이맵 4종(`NoColorSpace`) + 흑백 표면맵 1종
- uniforms: 두 상태 쌍(`uDispAlphaA/B`, `uDispScaleA/B`, `uMorph`...) + 토글들(`uForestOn·uRockOn·uGradeOn·uLightOn·uFogOn·uAnimOn`)
- `PlaneGeometry(800,1200,320,460)` ≈ 14.8만 정점, `rotation.x=-PI/2`, `position.y=-18`
## 5. `Sky.js`
큰 구(BackSide) 안쪽 그라데이션 — 바라보는 높이로 천정↔지평선 섞기 + 지평선 발광. 색은 메뉴마다 트윈.
## 6. `Water.js`
**핵심: 그 아래 지형 높이를 셰이더에서 다시 계산**(Terrain과 같은 두 상태 블렌드)해 해안선:
```glsl
float depth = uWaterLevel - terrainHeight(vWorld); // 두 상태 블렌드로 계산
if (depth < -1.5) discard; // 산이 높으면 물 안 그림(땅)
// + 얕을수록 거품, 프레넬 하늘반사, 높이텍스처 2번 스크롤 잔물결
```
- 머티리얼 **polygonOffset** → 해안선 z-fighting(면 충돌 깜빡임) 방지
- `uWaterLevel`과 `mesh.position.y`를 applyPreset에서 동기화 트윈
## 7. `Fluid.js` — GPU 유체
8종 풀스크린 셰이더를 텍스처(FBO)에 ping-pong:
```
splat → curl → vorticity → divergence → pressure(Jacobi 20회) → gradientSubtract → advect velocity → advect dye
```
- 속도장/염료장/압력장은 read/write 두 장씩 swap, `HalfFloatType`, simRes 256
- 결과 `dyeTexture`(안개) + `velocityTexture`(왜곡)을 Post가 가져감
## 8. `Post.js` — 후처리
```js
RenderPass → ShaderPass(Composite: dye 덧칠+velocity 왜곡) → UnrealBloomPass(0.15) → OutputPass(톤매핑)
```
9번 토글이 유체+합성을 함께 켬.
## 9. 나머지 파일
### projects.js — 원본에서 추출한 실프리셋
```js
{ name:'Victorinox', disp:0, dispScale:77.2, dispOff:[.283,.543], fog:'#E8664B', texScale:1.0, contrast:2.29 }
```
- 표면 텍스처 = `base-grayscale.jpg`(흑백). 원본도 컬러를 **채도 0**으로 흑백화 → 색은 fog에서.
### ui.js
- 각 메뉴 `<a>`에 **click** → 프리셋 적용(호버 아님, 유지). LoadingManager, 자동 입장.
### panel.js
- `[{id,label,on,off}]`로 동작. NEXT(한 겹씩)·RESET(1번만)·ALL.
## 10. GLSL 미니 사전
| 함수 | 뜻 |
|------|-----|
| `mix(a,b,t)` | a,b를 t비율로 섞기 |
| `smoothstep(lo,hi,x)` | lo~hi에서 0→1 부드럽게 |
| `dot(a,b)` | 두 벡터 일치도(조명) |
| `normalize(v)` | 길이 1로(방향만) |
| `texture2D(t,uv)` | 텍스처 픽셀값 |
| `fract / floor` | 소수부 / 내림(셀 격자) |
| `discard` | 이 픽셀 안 그림 |
## 11. 부록: "이 효과 바꾸려면 어디를?"
| 바꾸고 싶은 것 | 위치 |
|----------------|------|
| 나무 밀도 | `Terrain.js` `vUv * 240.0` |
| 나무 색 / 벚꽃 양 | `Terrain.js` `baseGreen`, `r3 > 0.93` |
| 암석 경사 문턱 | `Terrain.js` `smoothstep(0.40, 0.54, slope)` |
| 폭포 양 / 위치 | `Terrain.js` `valley`(0.15), `streak`(0.88) |
| 조명 세기 | `Terrain.js` `0.25 + 0.8*diff` + `uSunDir` |
| 안개 거리 | `Terrain.js` `uFogNear/uFogFar` (560/1450) |
| 해수면 높이 | `main.js` `applyPreset`의 `0.55` |
| 카메라 | `main.js` `camera.position` / `fov 30` |
| 모프 속도 | `main.js` `uMorph` 트윈 `duration: 1.6` |
| 무드 색 | `projects.js` 각 프리셋 `fog` |
| 블룸 세기 | `Post.js` `UnrealBloomPass(..., 0.15, ...)` |
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
(function(){
const src = document.getElementById('src').textContent;
const content = document.getElementById('content');
if (!window.marked){ const pre=document.createElement('pre'); pre.textContent=src; content.innerHTML=''; content.appendChild(pre); return; }
marked.setOptions({ gfm:true, breaks:false });
content.innerHTML = marked.parse(src);
const toc = document.getElementById('toc');
const heads = [...content.querySelectorAll('h2, h3')];
heads.forEach((h, i) => {
const id = 'sec-' + i; h.id = id;
const a = document.createElement('a'); a.href = '#' + id; a.textContent = h.textContent;
if (h.tagName === 'H3') a.className = 'h3';
a.addEventListener('click', (e) => { e.preventDefault(); h.scrollIntoView(); history.replaceState(null,'','#'+id); });
toc.appendChild(a);
});
const links = [...toc.querySelectorAll('a')];
const byId = Object.fromEntries(links.map(a => [a.getAttribute('href').slice(1), a]));
const obs = new IntersectionObserver((entries) => {
entries.forEach((en) => { if (en.isIntersecting) {
links.forEach(a => a.classList.remove('active'));
const a = byId[en.target.id]; if (a) { a.classList.add('active'); a.scrollIntoView({ block:'nearest' }); }
}});
}, { rootMargin: '0px 0px -75% 0px', threshold: 0 });
heads.forEach(h => obs.observe(h));
})();
</script>
</body>
</html>