๐Ÿงฑ Boilerplate : Shader Art

BamgasiJMยท2026๋…„ 6์›” 26์ผ

WebGL

๋ชฉ๋ก ๋ณด๊ธฐ
4/14
post-thumbnail

๐Ÿ“ index.html

<!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>

๐Ÿ“ style.css

* {
    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;
}

๐Ÿ“ app.js

/**
 * 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

๐Ÿ“ vert.glsl

#version 300 es
precision highp float;

in vec2 a_position;

void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
}

๐Ÿ“ frag.glsl

#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);
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€