<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shader Artwork</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<canvas id="webgl_canvas"></canvas>
<h1 class="title">Shader Artwork</h1>
</div>
<script src="app.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 40px 20px;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
}
#webgl_canvas {
display: block;
width: 1000px;
height: 1000px;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.title {
font-size: 32px;
font-weight: 300;
letter-spacing: 4px;
color: #e0e0e0;
text-transform: uppercase;
}
/**
* WebGL2 ์บ๋ฒ์ค ์ด๊ธฐํ ๋ฐ ์
ฐ์ด๋ ๋ ๋๋ง ์์
* - HTML์์ id๊ฐ "webgl_canvas"์ธ canvas ์์๋ฅผ ๊ฐ์ ธ์ด
* - WebGL2 ์ปจํ
์คํธ๋ฅผ ์์ฑํ๊ณ , ์
ฐ์ด๋๋ฅผ ๋ก๋/์ปดํ์ผํ์ฌ ํ์คํฌ๋ฆฐ ์ฟผ๋๋ฅผ ๊ทธ๋ฆฝ๋๋ค.
* - ์ ๋๋ฉ์ด์
๋ฃจํ์์ ์๊ฐ์ ๋ฐ๋ผ ๋ณํ๋ ์ ๋ํผ์ ์ ๋ฌํฉ๋๋ค.
*/
// ========================================
// 1. ์บ๋ฒ์ค ๋ฐ WebGL2 ์ปจํ
์คํธ ํ๋
// ========================================
// HTML ๋ฌธ์์์ id๊ฐ "webgl_canvas"์ธ canvas ์์๋ฅผ ๊ฐ์ ธ์ด
const canvas = document.getElementById("webgl_canvas");
// canvas์์ WebGL2 ๋ ๋๋ง ์ปจํ
์คํธ๋ฅผ ์์ฒญ
// gl ๋ณ์๋ ๋ชจ๋ WebGL API ํธ์ถ์ ์ง์
์ ์ญํ ์ ํจ
const gl = canvas.getContext("webgl2");
// WebGL2๋ฅผ ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ์ธ ๊ฒฝ์ฐ ๊ฒฝ๊ณ ๋ฐ ์๋ฌ ๋ฐ์
if (!gl) {
alert("WebGL2๋ฅผ ์ง์ํ์ง ์๋ ๋ธ๋ผ์ฐ์ ์
๋๋ค.");
throw new Error("WebGL2 not supported");
}
// ์บ๋ฒ์ค์ ์ค์ ํฝ์
ํฌ๊ธฐ๋ฅผ 1000x1000์ผ๋ก ์ค์ (CSS ํฌ๊ธฐ์๋ ๋ณ๊ฐ)
canvas.width = 1000;
canvas.height = 1000;
// ๋ทฐํฌํธ๋ฅผ ์บ๋ฒ์ค ์ ์ฒด ํฌ๊ธฐ๋ก ์ค์ (ํฝ์
์ขํ ๋งคํ)
gl.viewport(0, 0, canvas.width, canvas.height);
// ========================================
// 2. ์
ฐ์ด๋ ๋ก๋ ๋ฐ ์ปดํ์ผ ์ ํธ๋ฆฌํฐ ํจ์
// ========================================
/**
* ์ฃผ์ด์ง URL์์ ํ
์คํธ ํ์ผ์ ๋น๋๊ธฐ๋ก ๊ฐ์ ธ์ด (์
ฐ์ด๋ ์์ค์ฉ)
* @param {string} url - ์
ฐ์ด๋ ํ์ผ ๊ฒฝ๋ก
* @returns {Promise<string>} ํ์ผ ๋ด์ฉ
*/
async function loadShader(url) {
const response = await fetch(url); // HTTP ์์ฒญ์ผ๋ก ํ์ผ ์ฝ๊ธฐ
return await response.text(); // ์๋ต ๋ณธ๋ฌธ์ ํ
์คํธ๋ก ๋ฐํ
}
/**
* WebGL ์
ฐ์ด๋ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์ปดํ์ผ
* @param {number} type - gl.VERTEX_SHADER ๋๋ gl.FRAGMENT_SHADER
* @param {string} source - ์
ฐ์ด๋ ์์ค ์ฝ๋
* @returns {WebGLShader|null} ์ปดํ์ผ๋ ์
ฐ์ด๋ ๊ฐ์ฒด ๋๋ null (์ค๋ฅ ์)
*/
function compileShader(type, source) {
// ๋น ์
ฐ์ด๋ ๊ฐ์ฒด ์์ฑ
const shader = gl.createShader(type);
// ์
ฐ์ด๋ ๊ฐ์ฒด์ ์์ค ์ฝ๋ ํ ๋น
gl.shaderSource(shader, source);
// ์
ฐ์ด๋ ์ปดํ์ผ ์คํ
gl.compileShader(shader);
// ์ปดํ์ผ ์ฑ๊ณต ์ฌ๋ถ ํ์ธ
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
// ์คํจ ์ ๋ก๊ทธ ์ถ๋ ฅ ํ ์
ฐ์ด๋ ์ญ์
console.error("Shader compilation error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader; // ์ฑ๊ณต ์ ์
ฐ์ด๋ ๊ฐ์ฒด ๋ฐํ
}
/**
* ์ ์ ์
ฐ์ด๋์ ํ๋๊ทธ๋จผํธ ์
ฐ์ด๋๋ฅผ ์ฐ๊ฒฐํ์ฌ ํ๋ก๊ทธ๋จ ์์ฑ
* @param {WebGLShader} vertexShader - ์ปดํ์ผ๋ ์ ์ ์
ฐ์ด๋
* @param {WebGLShader} fragmentShader - ์ปดํ์ผ๋ ํ๋๊ทธ๋จผํธ ์
ฐ์ด๋
* @returns {WebGLProgram|null} ๋งํฌ๋ ํ๋ก๊ทธ๋จ ๊ฐ์ฒด ๋๋ null (์ค๋ฅ ์)
*/
function createProgram(vertexShader, fragmentShader) {
// ๋น ํ๋ก๊ทธ๋จ ๊ฐ์ฒด ์์ฑ
const program = gl.createProgram();
// ํ๋ก๊ทธ๋จ์ ๋ ์
ฐ์ด๋ ์ฐ๊ฒฐ
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// ์
ฐ์ด๋๋ค์ ๋งํฌํ์ฌ ์คํ ๊ฐ๋ฅํ ํ๋ก๊ทธ๋จ์ผ๋ก ๋ง๋ฆ
gl.linkProgram(program);
// ๋งํฌ ์ฑ๊ณต ์ฌ๋ถ ํ์ธ
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program linking error:", gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program; // ์ฑ๊ณต ์ ํ๋ก๊ทธ๋จ ๊ฐ์ฒด ๋ฐํ
}
// ========================================
// 3. ์ด๊ธฐํ ๋ฐ ๋ ๋๋ง ์ค์
// ========================================
/**
* ๋ชจ๋ WebGL ๋ฆฌ์์ค๋ฅผ ์ด๊ธฐํํ๊ณ ์ ๋๋ฉ์ด์
๋ฃจํ ์์
* - async ํจ์๋ก ์
ฐ์ด๋ ํ์ผ์ ๋น๋๊ธฐ ๋ก๋
*/
async function init() {
// ์
ฐ์ด๋ ํ์ผ๋ค์ ๋น๋๊ธฐ๋ก ์ฝ์ด์ด (๊ฒฝ๋ก๋ HTML ๊ธฐ์ค ์๋ ๊ฒฝ๋ก)
const vertexSource = await loadShader("shaders/vert.glsl");
const fragmentSource = await loadShader("shaders/frag.glsl");
// ์ ์ ์
ฐ์ด๋ ์ปดํ์ผ
const vertexShader = compileShader(gl.VERTEX_SHADER, vertexSource);
// ํ๋๊ทธ๋จผํธ ์
ฐ์ด๋ ์ปดํ์ผ
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSource);
// ๋ ์
ฐ์ด๋๋ฅผ ๋งํฌํ์ฌ ํ๋ก๊ทธ๋จ ์์ฑ
const program = createProgram(vertexShader, fragmentShader);
// ------------------------------------------------------------------
// ํ์คํฌ๋ฆฐ ์ฌ๊ฐํ (Full-screen quad) ๋ฐ์ดํฐ ์ค๋น
// - TRIANGLE_STRIP์ผ๋ก ๊ทธ๋ฆด 4๊ฐ์ ์ ์ (2D ์ขํ, x,y)
// - ์ขํ ๋ฒ์: -1 ~ 1 (NDC: Normalized Device Coordinates)
// ์์: ์ขํ(-1,-1), ์ฐํ(1,-1), ์ข์(-1,1), ์ฐ์(1,1)
// ------------------------------------------------------------------
const positions = new Float32Array([
-1, -1, // 0: ์ขํ
1, -1, // 1: ์ฐํ
-1, 1, // 2: ์ข์
1, 1 // 3: ์ฐ์
]);
// ๋ฒํผ ๊ฐ์ฒด ์์ฑ (GPU ๋ฉ๋ชจ๋ฆฌ์ ๋ฐ์ดํฐ ์ ์ฅ)
const positionBuffer = gl.createBuffer();
// ARRAY_BUFFER ํ์
์ผ๋ก ๋ฐ์ธ๋ฉ (์ ์ ์์ฑ ๋ฐ์ดํฐ)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// ๋ฒํผ์ ๋ฐ์ดํฐ ์
๋ก๋ (STATIC_DRAW: ํ ๋ฒ ์ฐ๊ณ ์ฌ๋ฌ ๋ฒ ์ฝ์)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
// VAO(Vertex Array Object) ์์ฑ - ์ ์ ์์ฑ ์ํ๋ฅผ ๋ฌถ์ด๋
const vao = gl.createVertexArray();
gl.bindVertexArray(vao); // ํ์ฌ VAO๋ก ํ์ฑํ
// ํ๋ก๊ทธ๋จ์์ 'a_position' ์์ฑ์ ์์น(index)๋ฅผ ๊ฐ์ ธ์ด
const a_position = gl.getAttribLocation(program, "a_position");
// ํด๋น ์์ฑ์ ํ์ฑํ (์ฌ์ฉํ๊ฒ ๋ค๊ณ ํ์)
gl.enableVertexAttribArray(a_position);
// ์์ฑ์ ๋ฐ์ดํฐ ํ์ ์ง์ : (index, size, type, normalized, stride, offset)
// size=2 (x,y), type=FLOAT, stride=0 (๋ฐ์ดํฐ๊ฐ ์ฐ์), offset=0
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0);
// ------------------------------------------------------------------
// ์ ๋ํผ ๋ณ์ ์์น ๊ฐ์ ธ์ค๊ธฐ (์
ฐ์ด๋์์ uniform์ผ๋ก ์ ์ธ๋ ๋ณ์)
// ------------------------------------------------------------------
const u_resolution = gl.getUniformLocation(program, "u_resolution");
const u_time = gl.getUniformLocation(program, "u_time");
// ------------------------------------------------------------------
// ๋ ๋ ๋ฃจํ (์ ๋๋ฉ์ด์
)
// ------------------------------------------------------------------
const startTime = Date.now(); // ์์ ์๊ฐ (๋ฐ๋ฆฌ์ด)
/**
* ๋งค ํ๋ ์ ํธ์ถ๋๋ ๋ ๋๋ง ํจ์
* - requestAnimationFrame์ ์ํด ๋ฐ๋ณต ์คํ (์ฌ๊ท์ ํธ์ถ์ด ์๋)
* (๋ธ๋ผ์ฐ์ ๊ฐ ๋ค์ ํ๋ฉด ๊ฐฑ์ ์์ ์ ์ฝ๋ฐฑ์ ํธ์ถ)
*/
function render() {
// ๊ฒฝ๊ณผ ์๊ฐ ๊ณ์ฐ (์ด ๋จ์)
const time = (Date.now() - startTime) * 0.001;
// ๋ฐฐ๊ฒฝ์ ์ค์ (๊ฒ์ ) ๋ฐ ๋ฒํผ ์ง์ฐ๊ธฐ
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// ํ์ฌ ํ๋ก๊ทธ๋จ ์ฌ์ฉ
gl.useProgram(program);
// ์ ๋ํผ ๊ฐ ์ ๋ฌ: ํด์๋(์บ๋ฒ์ค ํฌ๊ธฐ)์ ์๊ฐ
gl.uniform2f(u_resolution, canvas.width, canvas.height);
gl.uniform1f(u_time, time);
// VAO ๋ฐ์ธ๋ฉ (์ ์ ๋ฒํผ์ ์์ฑ ์ค์ ๋ณต์)
gl.bindVertexArray(vao);
// TRIANGLE_STRIP์ผ๋ก 4๊ฐ ์ ์ ์ ๊ทธ๋ฆฌ๊ธฐ (0๋ฒ๋ถํฐ 4๊ฐ)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// ๋ค์ ํ๋ ์ ์์ฒญ (๋ธ๋ผ์ฐ์ ๊ฐ repaint ์ง์ ์ render ๋ค์ ํธ์ถ)
// โป ์ด๋ ์ฌ๊ท ํจ์๊ฐ ์๋๋ผ, ์ด๋ฒคํธ ๋ฃจํ์ ์ํ ๋ฐ๋ณต ํธ์ถ์
๋๋ค.
requestAnimationFrame(render);
}
// ์ฒซ ๋ ๋๋ง ์์
render();
}
// ๋ชจ๋ ์ด๊ธฐํ ์คํ (๋น๋๊ธฐ ํจ์์ด๋ฏ๋ก then/catch ๋๋ await ์ฌ์ฉ ๊ฐ๋ฅ)
init();
shaders/vert.glsl
shaders/frag.glsl
#version 300 es
precision highp float;
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
#version 300 es
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
out vec4 fragColor;
void main() {
// ์ ๊ทํ๋ ์ขํ (0.0 ~ 1.0)
vec2 uv = gl_FragCoord.xy / u_resolution;
// ์๊ฐ์ ๋ฐ๋ผ ๋ณํ๋ ์์
vec3 color = vec3(
0.5 + 0.5 * sin(u_time),
0.5 + 0.5 * sin(u_time + 2.0),
0.5 + 0.5 * sin(u_time + 4.0)
);
fragColor = vec4(color, 1.0);
}