idgmatrix commited on
Commit
9e1cdc2
·
verified ·
1 Parent(s): 198d6b5

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +675 -19
index.html CHANGED
@@ -1,19 +1,675 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WebGPU Black Hole Simulation</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Noto+Sans+KR:wght@300;500;700&display=swap');
9
+
10
+ :root {
11
+ --primary-color: #00d2ff;
12
+ --secondary-color: #ff0055;
13
+ --bg-color: #050505;
14
+ --panel-bg: rgba(20, 20, 30, 0.75);
15
+ --text-color: #ffffff;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ margin: 0;
21
+ padding: 0;
22
+ }
23
+
24
+ body {
25
+ background-color: var(--bg-color);
26
+ color: var(--text-color);
27
+ font-family: 'Noto Sans KR', sans-serif;
28
+ overflow: hidden;
29
+ width: 100vw;
30
+ height: 100vh;
31
+ }
32
+
33
+ /* Header / Branding */
34
+ header {
35
+ position: absolute;
36
+ top: 0;
37
+ left: 0;
38
+ width: 100%;
39
+ padding: 1.5rem;
40
+ display: flex;
41
+ justify-content: space-between;
42
+ align-items: center;
43
+ z-index: 100;
44
+ pointer-events: none; /* click through to canvas */
45
+ }
46
+
47
+ .brand {
48
+ font-family: 'Orbitron', sans-serif;
49
+ font-weight: 700;
50
+ font-size: 1.5rem;
51
+ letter-spacing: 2px;
52
+ text-shadow: 0 0 10px rgba(0, 210, 255, 0.5);
53
+ pointer-events: auto;
54
+ }
55
+
56
+ .anycoder-link {
57
+ font-family: 'Orbitron', sans-serif;
58
+ font-size: 0.9rem;
59
+ color: var(--text-color);
60
+ text-decoration: none;
61
+ border: 1px solid rgba(255, 255, 255, 0.3);
62
+ padding: 8px 16px;
63
+ border-radius: 20px;
64
+ background: rgba(0, 0, 0, 0.5);
65
+ backdrop-filter: blur(5px);
66
+ transition: all 0.3s ease;
67
+ pointer-events: auto;
68
+ }
69
+
70
+ .anycoder-link:hover {
71
+ background: var(--primary-color);
72
+ color: #000;
73
+ box-shadow: 0 0 15px var(--primary-color);
74
+ border-color: var(--primary-color);
75
+ }
76
+
77
+ /* Canvas */
78
+ canvas {
79
+ display: block;
80
+ width: 100%;
81
+ height: 100%;
82
+ }
83
+
84
+ /* UI Overlay */
85
+ .controls {
86
+ position: absolute;
87
+ bottom: 2rem;
88
+ left: 2rem;
89
+ width: 300px;
90
+ background: var(--panel-bg);
91
+ backdrop-filter: blur(10px);
92
+ -webkit-backdrop-filter: blur(10px);
93
+ padding: 1.5rem;
94
+ border-radius: 12px;
95
+ border: 1px solid rgba(255, 255, 255, 0.1);
96
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
97
+ z-index: 100;
98
+ transition: opacity 0.3s ease;
99
+ }
100
+
101
+ .controls h2 {
102
+ font-family: 'Orbitron', sans-serif;
103
+ font-size: 1.1rem;
104
+ margin-bottom: 1rem;
105
+ color: var(--primary-color);
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 10px;
109
+ }
110
+
111
+ .control-group {
112
+ margin-bottom: 1rem;
113
+ }
114
+
115
+ .control-group label {
116
+ display: flex;
117
+ justify-content: space-between;
118
+ font-size: 0.85rem;
119
+ margin-bottom: 0.5rem;
120
+ color: #ccc;
121
+ }
122
+
123
+ .control-group input[type="range"] {
124
+ width: 100%;
125
+ -webkit-appearance: none;
126
+ background: transparent;
127
+ height: 6px;
128
+ border-radius: 3px;
129
+ background: rgba(255, 255, 255, 0.1);
130
+ outline: none;
131
+ }
132
+
133
+ .control-group input[type="range"]::-webkit-slider-thumb {
134
+ -webkit-appearance: none;
135
+ width: 16px;
136
+ height: 16px;
137
+ border-radius: 50%;
138
+ background: var(--primary-color);
139
+ cursor: pointer;
140
+ box-shadow: 0 0 10px var(--primary-color);
141
+ margin-top: -5px;
142
+ transition: transform 0.1s;
143
+ }
144
+
145
+ .control-group input[type="range"]::-webkit-slider-thumb:hover {
146
+ transform: scale(1.2);
147
+ }
148
+
149
+ .control-group input[type="range"]::-webkit-slider-runnable-track {
150
+ width: 100%;
151
+ height: 6px;
152
+ cursor: pointer;
153
+ }
154
+
155
+ .status {
156
+ position: absolute;
157
+ top: 50%;
158
+ left: 50%;
159
+ transform: translate(-50%, -50%);
160
+ text-align: center;
161
+ z-index: 200;
162
+ background: rgba(0,0,0,0.9);
163
+ padding: 2rem;
164
+ border-radius: 10px;
165
+ border: 1px solid var(--secondary-color);
166
+ display: none; /* Hidden by default */
167
+ }
168
+
169
+ .status.error {
170
+ display: block;
171
+ color: var(--secondary-color);
172
+ }
173
+
174
+ .status h3 {
175
+ margin-bottom: 1rem;
176
+ font-family: 'Orbitron', sans-serif;
177
+ }
178
+
179
+ /* Loading Spinner */
180
+ .loader {
181
+ position: absolute;
182
+ top: 50%;
183
+ left: 50%;
184
+ transform: translate(-50%, -50%);
185
+ width: 50px;
186
+ height: 50px;
187
+ border: 3px solid rgba(255,255,255,0.1);
188
+ border-radius: 50%;
189
+ border-top-color: var(--primary-color);
190
+ animation: spin 1s ease-in-out infinite;
191
+ z-index: 150;
192
+ }
193
+
194
+ @keyframes spin {
195
+ to { transform: translate(-50%, -50%) rotate(360deg); }
196
+ }
197
+
198
+ .hidden {
199
+ display: none !important;
200
+ }
201
+
202
+ .instructions {
203
+ position: absolute;
204
+ bottom: 2rem;
205
+ right: 2rem;
206
+ text-align: right;
207
+ font-size: 0.8rem;
208
+ color: rgba(255,255,255,0.5);
209
+ pointer-events: none;
210
+ }
211
+ </style>
212
+ </head>
213
+ <body>
214
+
215
+ <header>
216
+ <div class="brand">GARGANTUA <span style="color:var(--primary-color); font-size: 0.8em;">WebGPU</span></div>
217
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
218
+ </header>
219
+
220
+ <div id="loader" class="loader"></div>
221
+
222
+ <div id="error-modal" class="status">
223
+ <h3>WebGPU 지원 불가</h3>
224
+ <p>이 브라우저는 WebGPU를 지원하지 않습니다.<br>Chrome 최신 버전이나 Edge를 사용해 주세요.</p>
225
+ </div>
226
+
227
+ <canvas id="gpuCanvas"></canvas>
228
+
229
+ <div class="controls">
230
+ <h2>
231
+ <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/></svg>
232
+ 제어 패널
233
+ </h2>
234
+
235
+ <div class="control-group">
236
+ <label>블랙홀 질량 (Mass) <span id="val-mass">1.0</span></label>
237
+ <input type="range" id="mass" min="0.1" max="3.0" step="0.1" value="1.0">
238
+ </div>
239
+
240
+ <div class="control-group">
241
+ <label>원반 밝기 (Intensity) <span id="val-intensity">1.5</span></label>
242
+ <input type="range" id="intensity" min="0.1" max="5.0" step="0.1" value="1.5">
243
+ </div>
244
+
245
+ <div class="control-group">
246
+ <label>가스 온도 (Temperature) <span id="val-temp">0.5</span></label>
247
+ <input type="range" id="temp" min="0.0" max="1.0" step="0.01" value="0.5">
248
+ </div>
249
+
250
+ <div class="control-group">
251
+ <label>시뮬레이션 속도 (Speed) <span id="val-speed">1.0</span></label>
252
+ <input type="range" id="speed" min="0.0" max="5.0" step="0.1" value="1.0">
253
+ </div>
254
+ </div>
255
+
256
+ <div class="instructions">
257
+ 드래그하여 회전 / 휠로 줌
258
+ </div>
259
+
260
+ <script type="module">
261
+ // WebGPU Setup and Logic
262
+ const canvas = document.getElementById('gpuCanvas');
263
+ const loader = document.getElementById('loader');
264
+ const errorModal = document.getElementById('error-modal');
265
+
266
+ // UI Elements
267
+ const ui = {
268
+ mass: document.getElementById('mass'),
269
+ intensity: document.getElementById('intensity'),
270
+ temp: document.getElementById('temp'),
271
+ speed: document.getElementById('speed'),
272
+ valMass: document.getElementById('val-mass'),
273
+ valIntensity: document.getElementById('val-intensity'),
274
+ valTemp: document.getElementById('val-temp'),
275
+ valSpeed: document.getElementById('val-speed'),
276
+ };
277
+
278
+ // State
279
+ const state = {
280
+ mass: 1.0,
281
+ intensity: 1.5,
282
+ temperature: 0.5,
283
+ speed: 1.0,
284
+ time: 0,
285
+ camRadius: 8.0,
286
+ camTheta: 0.0, // Horizontal
287
+ camPhi: 0.3, // Vertical
288
+ mouseDown: false,
289
+ lastMouseX: 0,
290
+ lastMouseY: 0
291
+ };
292
+
293
+ // Update UI Labels
294
+ const updateLabels = () => {
295
+ ui.valMass.innerText = state.mass.toFixed(1);
296
+ ui.valIntensity.innerText = state.intensity.toFixed(1);
297
+ ui.valTemp.innerText = state.temperature.toFixed(2);
298
+ ui.valSpeed.innerText = state.speed.toFixed(1);
299
+ };
300
+
301
+ // Event Listeners for UI
302
+ ui.mass.addEventListener('input', (e) => { state.mass = parseFloat(e.target.value); updateLabels(); });
303
+ ui.intensity.addEventListener('input', (e) => { state.intensity = parseFloat(e.target.value); updateLabels(); });
304
+ ui.temp.addEventListener('input', (e) => { state.temperature = parseFloat(e.target.value); updateLabels(); });
305
+ ui.speed.addEventListener('input', (e) => { state.speed = parseFloat(e.target.value); updateLabels(); });
306
+
307
+ // Mouse Controls
308
+ canvas.addEventListener('mousedown', (e) => {
309
+ state.mouseDown = true;
310
+ state.lastMouseX = e.clientX;
311
+ state.lastMouseY = e.clientY;
312
+ });
313
+ window.addEventListener('mouseup', () => state.mouseDown = false);
314
+ window.addEventListener('mousemove', (e) => {
315
+ if (!state.mouseDown) return;
316
+ const dx = e.clientX - state.lastMouseX;
317
+ const dy = e.clientY - state.lastMouseY;
318
+ state.lastMouseX = e.clientX;
319
+ state.lastMouseY = e.clientY;
320
+
321
+ state.camTheta -= dx * 0.005;
322
+ state.camPhi += dy * 0.005;
323
+ // Clamp vertical angle to avoid flipping
324
+ state.camPhi = Math.max(-1.5, Math.min(1.5, state.camPhi));
325
+ });
326
+ canvas.addEventListener('wheel', (e) => {
327
+ state.camRadius += e.deltaY * 0.01;
328
+ state.camRadius = Math.max(3.0, Math.min(20.0, state.camRadius));
329
+ });
330
+
331
+ // WGSL Shader Code
332
+ const shaderCode = `
333
+ struct Uniforms {
334
+ resolution: vec2f,
335
+ time: f32,
336
+ mass: f32,
337
+ intensity: f32,
338
+ temperature: f32,
339
+ camPos: vec3f,
340
+ camTarget: vec3f,
341
+ };
342
+
343
+ @group(0) @binding(0) var<uniform> u: Uniforms;
344
+
345
+ struct VertexOutput {
346
+ @builtin(position) position: vec4f,
347
+ @location(0) uv: vec2f,
348
+ };
349
+
350
+ @vertex
351
+ fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
352
+ var pos = array<vec2f, 6>(
353
+ vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
354
+ vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0)
355
+ );
356
+ var output: VertexOutput;
357
+ output.position = vec4f(pos[vertexIndex], 0.0, 1.0);
358
+ output.uv = pos[vertexIndex]; // -1 to 1
359
+ return output;
360
+ }
361
+
362
+ // --- Noise Functions for Accretion Disk ---
363
+ fn hash(p: vec2f) -> f32 {
364
+ var p3 = fract(vec3f(p.xyx) * .1031);
365
+ p3 += dot(p3, p3.yzx + 33.33);
366
+ return fract((p3.x + p3.y) * p3.z);
367
+ }
368
+
369
+ fn noise(p: vec2f) -> f32 {
370
+ let i = floor(p);
371
+ let f = fract(p);
372
+ let u = f * f * (3.0 - 2.0 * f);
373
+ return mix(
374
+ mix(hash(i + vec2f(0.0, 0.0)), hash(i + vec2f(1.0, 0.0)), u.x),
375
+ mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
376
+ u.y
377
+ );
378
+ }
379
+
380
+ fn fbm(p: vec2f) -> f32 {
381
+ var value: f32 = 0.0;
382
+ var amplitude: f32 = 0.5;
383
+ var st = p;
384
+ for (var i = 0; i < 5; i++) {
385
+ value += amplitude * noise(st);
386
+ st *= 2.0;
387
+ amplitude *= 0.5;
388
+ }
389
+ return value;
390
+ }
391
+
392
+ // --- Physics & Rendering ---
393
+
394
+ // Rotate vector
395
+ fn rotateY(v: vec3f, angle: f32) -> vec3f {
396
+ let c = cos(angle);
397
+ let s = sin(angle);
398
+ return vec3f(c * v.x + s * v.z, v.y, -s * v.x + c * v.z);
399
+ }
400
+
401
+ // Blackbody-ish color ramp
402
+ fn blackBodyColor(t: f32) -> vec3f {
403
+ // Map t (0 to 1) to colors: Red -> Orange -> White -> Blue
404
+ let r = smoothstep(0.0, 0.5, t) + smoothstep(0.3, 1.0, t);
405
+ let g = smoothstep(0.2, 0.7, t) + smoothstep(0.8, 1.0, t) * 0.5;
406
+ let b = smoothstep(0.5, 0.9, t) + smoothstep(0.9, 1.0, t) * 2.0;
407
+ return vec3f(r, g, b) * u.intensity;
408
+ }
409
+
410
+ @fragment
411
+ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
412
+ // 1. Setup Camera
413
+ let uv = in.uv * vec2f(u.resolution.x / u.resolution.y, 1.0);
414
+
415
+ let ro = u.camPos;
416
+ let ta = u.camTarget;
417
+
418
+ let ww = normalize(ta - ro);
419
+ let uu = normalize(cross(ww, vec3f(0.0, 1.0, 0.0)));
420
+ let vv = normalize(cross(uu, ww));
421
+
422
+ var rd = normalize(uv.x * uu + uv.y * vv + 1.5 * ww);
423
+
424
+ // 2. Ray Marching Simulation (Curved Space-Time approximation)
425
+ var pos = ro;
426
+ var color = vec3f(0.0);
427
+ var glow = 0.0;
428
+
429
+ let dt = 0.05; // Step size
430
+ let maxSteps = 300;
431
+
432
+ // Accretion disk parameters
433
+ let innerRadius = 2.0 * u.mass; // Schwarzschild radius * scaling
434
+ let diskInner = 2.6 * u.mass;
435
+ let diskOuter = 8.0 * u.mass;
436
+
437
+ var hitHorizon = false;
438
+
439
+ for(var i = 0; i < maxSteps; i++) {
440
+ let distSq = dot(pos, pos);
441
+ let dist = sqrt(distSq);
442
+
443
+ // Event Horizon Check
444
+ if (dist < innerRadius) {
445
+ hitHorizon = true;
446
+ break;
447
+ }
448
+
449
+ // Escape Check
450
+ if (dist > 25.0) {
451
+ break;
452
+ }
453
+
454
+ // Gravity Bending (Newtonian approx for visual flair)
455
+ // Acceleration towards center
456
+ let force = (1.5 * u.mass) / (distSq * 1.5); // Tweaked for visuals
457
+ let acc = -normalize(pos) * force;
458
+
459
+ // Update Velocity (Ray direction)
460
+ rd += acc * dt;
461
+ rd = normalize(rd);
462
+
463
+ // Update Position
464
+ let prevPos = pos;
465
+ pos += rd * dt;
466
+
467
+ // --- Accretion Disk Rendering ---
468
+ // Check if we crossed the Y=0 plane
469
+ if (prevPos.y * pos.y < 0.0 || abs(pos.y) < 0.1) {
470
+ let midPoint = (prevPos + pos) * 0.5;
471
+ let r = length(midPoint.xz);
472
+
473
+ if (r > diskInner && r < diskOuter) {
474
+ // Calculate polar coordinates for noise
475
+ let angle = atan2(midPoint.z, midPoint.x);
476
+
477
+ // Rotating texture
478
+ let uvDisk = vec2f(r - diskInner, angle + u.time * 0.5 + 10.0/r);
479
+
480
+ // Noise generation
481
+ var density = fbm(uvDisk * vec2f(1.0, 3.0));
482
+
483
+ // Radial fade out
484
+ let fade = smoothstep(diskOuter, diskOuter - 1.0, r) * smoothstep(diskInner, diskInner + 0.5, r);
485
+ density *= fade;
486
+
487
+ // Doppler Beaming Approximation
488
+ // Matter on left moves towards us (brighter), right moves away (dimmer)
489
+ // Assuming camera is somewhat aligned with z-axis logic
490
+ let velDir = normalize(vec3f(-midPoint.z, 0.0, midPoint.x)); // Tangential velocity
491
+ let viewDir = normalize(ro - midPoint);
492
+ let doppler = 1.0 + dot(velDir, viewDir) * 0.5;
493
+
494
+ // Color mapping
495
+ let temp = density * doppler;
496
+ let diskCol = blackBodyColor(temp * (0.5 + u.temperature));
497
+
498
+ // Accumulate (Volumetric-ish addition)
499
+ color += diskCol * density * 0.4 * u.intensity;
500
+ }
501
+ }
502
+
503
+ // Accumulate Glow around the black hole
504
+ glow += 0.01 / (dist * dist * 0.1 + 0.01);
505
+ }
506
+
507
+ // Stars / Background
508
+ if (!hitHorizon) {
509
+ let starDir = rd;
510
+ let starVal = pow(hash(starDir.xy * 50.0 + starDir.zz * 50.0), 50.0) * 2.0;
511
+ color += vec3f(starVal);
512
+ }
513
+
514
+ // Add Glow
515
+ color += vec3f(0.1, 0.3, 0.5) * glow * 0.5;
516
+
517
+ // Tone mapping
518
+ color = color / (color + vec3f(1.0));
519
+ color = pow(color, vec3f(0.4545)); // Gamma correction
520
+
521
+ return vec4f(color, 1.0);
522
+ }
523
+ `;
524
+
525
+ async function init() {
526
+ if (!navigator.gpu) {
527
+ loader.classList.add('hidden');
528
+ errorModal.classList.add('error');
529
+ return;
530
+ }
531
+
532
+ try {
533
+ const adapter = await navigator.gpu.requestAdapter();
534
+ if (!adapter) throw new Error("No adapter found");
535
+
536
+ const device = await adapter.requestDevice();
537
+ const context = canvas.getContext('webgpu');
538
+
539
+ const format = navigator.gpu.getPreferredCanvasFormat();
540
+
541
+ context.configure({
542
+ device: device,
543
+ format: format,
544
+ alphaMode: 'opaque',
545
+ });
546
+
547
+ // Create Shader Module
548
+ const shaderModule = device.createShaderModule({
549
+ label: 'Black Hole Shader',
550
+ code: shaderCode
551
+ });
552
+
553
+ // Create Pipeline
554
+ const pipeline = device.createRenderPipeline({
555
+ label: 'Render Pipeline',
556
+ layout: 'auto',
557
+ vertex: {
558
+ module: shaderModule,
559
+ entryPoint: 'vs_main',
560
+ },
561
+ fragment: {
562
+ module: shaderModule,
563
+ entryPoint: 'fs_main',
564
+ targets: [{ format: format }],
565
+ },
566
+ primitive: {
567
+ topology: 'triangle-list',
568
+ }
569
+ });
570
+
571
+ // Uniform Buffer Setup
572
+ // Struct: resolution(vec2), time(f32), mass(f32), intensity(f32), temperature(f32), camPos(vec3), camTarget(vec3)
573
+ // Padding rules: vec3 takes 16 bytes (4 floats) alignment.
574
+ // Layout:
575
+ // 0: res.x, res.y, time, mass
576
+ // 16: intensity, temperature, padding, padding
577
+ // 32: camPos.x, camPos.y, camPos.z, padding
578
+ // 48: camTarget.x, camTarget.y, camTarget.z, padding
579
+ // Total: 64 bytes
580
+ const uniformBufferSize = 64;
581
+ const uniformBuffer = device.createBuffer({
582
+ size: uniformBufferSize,
583
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
584
+ });
585
+
586
+ const bindGroup = device.createBindGroup({
587
+ layout: pipeline.getBindGroupLayout(0),
588
+ entries: [{
589
+ binding: 0,
590
+ resource: { buffer: uniformBuffer }
591
+ }]
592
+ });
593
+
594
+ const uniformValues = new Float32Array(uniformBufferSize / 4);
595
+
596
+ loader.classList.add('hidden');
597
+
598
+ function render(timestamp) {
599
+ state.time += 0.01 * state.speed;
600
+
601
+ // Update Canvas Size
602
+ const width = canvas.clientWidth;
603
+ const height = canvas.clientHeight;
604
+ canvas.width = width;
605
+ canvas.height = height;
606
+
607
+ // Update Camera Position (Spherical coords)
608
+ const camX = state.camRadius * Math.sin(state.camTheta) * Math.cos(state.camPhi);
609
+ const camY = state.camRadius * Math.sin(state.camPhi);
610
+ const camZ = state.camRadius * Math.cos(state.camTheta) * Math.cos(state.camPhi);
611
+
612
+ // Update Uniforms
613
+ // 0: res.x, res.y, time, mass
614
+ uniformValues[0] = width;
615
+ uniformValues[1] = height;
616
+ uniformValues[2] = state.time;
617
+ uniformValues[3] = state.mass;
618
+
619
+ // 4: intensity, temp, padding, padding
620
+ uniformValues[4] = state.intensity;
621
+ uniformValues[5] = state.temperature;
622
+ uniformValues[6] = 0; // padding
623
+ uniformValues[7] = 0; // padding
624
+
625
+ // 8: camPos (vec3 + padding)
626
+ uniformValues[8] = camX;
627
+ uniformValues[9] = camY;
628
+ uniformValues[10] = camZ;
629
+ uniformValues[11] = 0;
630
+
631
+ // 12: camTarget (vec3 + padding)
632
+ uniformValues[12] = 0;
633
+ uniformValues[13] = 0;
634
+ uniformValues[14] = 0;
635
+ uniformValues[15] = 0;
636
+
637
+ device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
638
+
639
+ const commandEncoder = device.createCommandEncoder();
640
+ const textureView = context.getCurrentTexture().createView();
641
+
642
+ const renderPassDescriptor = {
643
+ colorAttachments: [{
644
+ view: textureView,
645
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
646
+ loadOp: 'clear',
647
+ storeOp: 'store',
648
+ }]
649
+ };
650
+
651
+ const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
652
+ passEncoder.setPipeline(pipeline);
653
+ passEncoder.setBindGroup(0, bindGroup);
654
+ passEncoder.draw(6); // Draw 6 vertices (2 triangles = 1 quad)
655
+ passEncoder.end();
656
+
657
+ device.queue.submit([commandEncoder.finish()]);
658
+
659
+ requestAnimationFrame(render);
660
+ }
661
+
662
+ requestAnimationFrame(render);
663
+
664
+ } catch (e) {
665
+ console.error(e);
666
+ loader.classList.add('hidden');
667
+ errorModal.innerHTML = `<h3>오류 발생</h3><p>${e.message}</p>`;
668
+ errorModal.classList.add('error');
669
+ }
670
+ }
671
+
672
+ init();
673
+ </script>
674
+ </body>
675
+ </html>