검색
검색
공개 노트 검색
회원가입로그인

WebGL로 만드는 아티스틱 파티클 시뮬레이터

요약
  • WebGL을 사용하여 아름답고 인터랙티브한 파티클 시뮬레이터를 만드는 방법을 소개합니다.
  • 쉐이더 프로그램, 파티클 시스템 설정, 렌더링 및 인터랙티브 기능 추가의 단계별 설명을 제공합니다.
  • 전체 코드와 함께 마우스 인터랙션을 추가하여 파티클이 마우스 커서를 따라가도록 하는 방법을 설명합니다.

chrome_sAMAeYopHO

WebGL로 만드는 아티스틱 파티클 시뮬레이터

안녕하세요! 오늘은 WebGL을 사용하여 아름답고 인터랙티브한 파티클 시뮬레이터를 만드는 방법을 알아보겠습니다. 이 튜토리얼은 WebGL 초보자를 위해 작성되었으며, 단계별로 코드를 설명할 것입니다.

목차

  1. 소개

  2. 기본 설정

  3. 쉐이더 프로그램

  4. 파티클 시스템

  5. 렌더링

  6. 인터랙션 추가

  7. 전체 코드

  8. 응용 방법

소개

WebGL은 웹 브라우저에서 고성능 3D 그래픽을 렌더링할 수 있게 해주는 JavaScript API입니다. 오늘 우리는 이를 사용하여 2D 파티클 시스템을 만들 것입니다. 이 시스템은 다음과 같은 특징을 가집니다:

  • 전체 화면 캔버스

  • 다양한 크기와 색상의 파티클

  • 시간에 따른 색상 변화

  • 마우스 인터랙션

기본 설정

먼저 HTML 파일을 만들고 기본 구조를 설정합니다:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>아티스틱 파티클 시뮬레이터</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        // 여기에 JavaScript 코드가 들어갑니다
    </script>
</body>
</html>

이제 WebGL 컨텍스트를 초기화합니다:

const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');

if (!gl) {
    alert('WebGL을 초기화할 수 없습니다. 브라우저가 지원하지 않을 수 있습니다.');
    throw new Error('WebGL not supported');
}

쉐이더 프로그램

WebGL에서는 쉐이더를 사용하여 그래픽을 렌더링합니다. 우리는 두 가지 쉐이더를 만들 것입니다:

  1. 버텍스 쉐이더: 파티클의 위치와 크기를 결정합니다.

  2. 프래그먼트 쉐이더: 파티클의 색상과 모양을 결정합니다.

// 버텍스 쉐이더
const vsSource = `
    attribute vec2 a_position;
    attribute float a_size;
    attribute vec3 a_color;
    varying vec3 v_color;
    void main() {
        gl_Position = vec4(a_position, 0.0, 1.0);
        gl_PointSize = a_size;
        v_color = a_color;
    }
`;

// 프래그먼트 쉐이더
const fsSource = `
    precision mediump float;
    varying vec3 v_color;
    void main() {
        vec2 center = gl_PointCoord - vec2(0.5);
        float dist = length(center);
        if (dist > 0.5) discard;
        gl_FragColor = vec4(v_color, 1.0 - smoothstep(0.45, 0.5, dist));
    }
`;

이제 이 쉐이더들을 컴파일하고 링크하여 쉐이더 프로그램을 만듭니다:

function createShaderProgram(gl, vsSource, fsSource) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

    const shaderProgram = gl.createProgram();
    gl.attachShader(shaderProgram, vertexShader);
    gl.attachShader(shaderProgram, fragmentShader);
    gl.linkProgram(shaderProgram);

    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
        alert('쉐이더 프로그램을 초기화하는 데 실패했습니다: ' + gl.getProgramInfoLog(shaderProgram));
        return null;
    }

    return shaderProgram;
}

function loadShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        alert('쉐이더 컴파일 중 오류가 발생했습니다: ' + gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

const shaderProgram = createShaderProgram(gl, vsSource, fsSource);

파티클 시스템

이제 파티클 시스템을 설정합니다. 각 파티클은 위치, 속도, 크기, 색상을 가집니다:

const NUM_PARTICLES = 5000;
const particles = new Float32Array(NUM_PARTICLES * 2);
const velocities = new Float32Array(NUM_PARTICLES * 2);
const sizes = new Float32Array(NUM_PARTICLES);
const colors = new Float32Array(NUM_PARTICLES * 3);

function initParticles() {
    for (let i = 0; i < NUM_PARTICLES; i++) {
        particles[i * 2] = Math.random() * 2 - 1;
        particles[i * 2 + 1] = Math.random() * 2 - 1;
        velocities[i * 2] = (Math.random() - 0.5) * 0.01;
        velocities[i * 2 + 1] = (Math.random() - 0.5) * 0.01;
        sizes[i] = Math.random() * 10 + 2;
        colors[i * 3] = Math.random();
        colors[i * 3 + 1] = Math.random();
        colors[i * 3 + 2] = Math.random();
    }
}

initParticles();

그리고 이 데이터를 WebGL 버퍼에 넣습니다:

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, particles, gl.DYNAMIC_DRAW);

const sizeBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);

const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);

렌더링

이제 파티클을 화면에 그리는 함수를 만듭니다:

function draw(now) {
    resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(shaderProgram);

    // 파티클 업데이트 및 그리기
    for (let i = 0; i < NUM_PARTICLES; i++) {
        particles[i * 2] += velocities[i * 2];
        particles[i * 2 + 1] += velocities[i * 2 + 1];

        // 화면 경계에서 튕기기
        if (Math.abs(particles[i * 2]) > 1) velocities[i * 2] *= -1;
        if (Math.abs(particles[i * 2 + 1]) > 1) velocities[i * 2 + 1] *= -1;

        // 색상 변화
        colors[i * 3] = (Math.sin(now * 0.001 + i * 0.1) + 1) * 0.5;
        colors[i * 3 + 1] = (Math.sin(now * 0.002 + i * 0.1) + 1) * 0.5;
        colors[i * 3 + 2] = (Math.sin(now * 0.003 + i * 0.1) + 1) * 0.5;
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, particles);

    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);

    // 속성 설정
    const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position');
    const sizeAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_size');
    const colorAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_color');

    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(positionAttributeLocation);

    gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
    gl.vertexAttribPointer(sizeAttributeLocation, 1, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(sizeAttributeLocation);

    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.vertexAttribPointer(colorAttributeLocation, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(colorAttributeLocation);

    gl.drawArrays(gl.POINTS, 0, NUM_PARTICLES);

    requestAnimationFrame(draw);
}

function resizeCanvasToDisplaySize(canvas) {
    const displayWidth  = canvas.clientWidth;
    const displayHeight = canvas.clientHeight;

    if (canvas.width  !== displayWidth ||
        canvas.height !== displayHeight) {
        canvas.width  = displayWidth;
        canvas.height = displayHeight;
    }
}

// 애니메이션 시작
requestAnimationFrame(draw);

인터랙션 추가

마지막으로, 마우스 인터랙션을 추가하여 파티클이 마우스 커서를 따라가도록 만듭니다:

let mouseX = 0, mouseY = 0;
canvas.addEventListener('mousemove', (event) => {
    const rect = canvas.getBoundingClientRect();
    mouseX = (event.clientX - rect.left) / canvas.width * 2 - 1;
    mouseY = -((event.clientY - rect.top) / canvas.height * 2 - 1);
});

// draw 함수 내의 파티클 업데이트 부분에 다음 코드를 추가합니다
let dx = mouseX - particles[i * 2];
let dy = mouseY - particles[i * 2 + 1];
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.1) {
    velocities[i * 2] += dx * 0.0001 / dist;
    velocities[i * 2 + 1] += dy * 0.0001 / dist;
}

전체 코드

전체 코드는 다음과 같습니다:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>아티스틱 파티클 시뮬레이터</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        const canvas = document.getElementById('glCanvas');
        const gl = canvas.getContext('webgl');

        if (!gl) {
            alert('WebGL을 초기화할 수 없습니다. 브라우저가 지원하지 않을 수 있습니다.');
            throw new Error('WebGL not supported');
        }

        // 버텍스 쉐이더
        const vsSource = `
            attribute vec2 a_position;
            attribute float a_size;
            attribute vec3 a_color;
            varying vec3 v_color;
            void main() {
                gl_Position = vec4(a_position, 0.0, 1.0);
                gl_PointSize = a_size;
                v_color = a_color;
            }
        `;

        // 프래그먼트 쉐이더
        const fsSource = `
            precision mediump float;
            varying

실습코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Artistic Particle Simulator</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; }
        canvas { display: block; }
    </style>
</head>
<body>
    <canvas id="glCanvas"></canvas>
    <script>
        const canvas = document.getElementById('glCanvas');
        const gl = canvas.getContext('webgl');

        if (!gl) {
            alert('Unable to initialize WebGL. Your browser may not support it.');
            throw new Error('WebGL not supported');
        }

        // Vertex shader program
        const vsSource = `
            attribute vec2 a_position;
            attribute float a_size;
            attribute vec3 a_color;
            varying vec3 v_color;
            void main() {
                gl_Position = vec4(a_position, 0.0, 1.0);
                gl_PointSize = a_size;
                v_color = a_color;
            }
        `;

        // Fragment shader program
        const fsSource = `
            precision mediump float;
            varying vec3 v_color;
            void main() {
                vec2 center = gl_PointCoord - vec2(0.5);
                float dist = length(center);
                if (dist > 0.5) discard;
                gl_FragColor = vec4(v_color, 1.0 - smoothstep(0.45, 0.5, dist));
            }
        `;

        // Create shader program
        function createShaderProgram(gl, vsSource, fsSource) {
            const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
            const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

            const shaderProgram = gl.createProgram();
            gl.attachShader(shaderProgram, vertexShader);
            gl.attachShader(shaderProgram, fragmentShader);
            gl.linkProgram(shaderProgram);

            if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
                alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
                return null;
            }

            return shaderProgram;
        }

        function loadShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);

            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }

            return shader;
        }

        const shaderProgram = createShaderProgram(gl, vsSource, fsSource);

        // Particle system
        const NUM_PARTICLES = 5000;
        const particles = new Float32Array(NUM_PARTICLES * 2);
        const velocities = new Float32Array(NUM_PARTICLES * 2);
        const sizes = new Float32Array(NUM_PARTICLES);
        const colors = new Float32Array(NUM_PARTICLES * 3);

        function initParticles() {
            for (let i = 0; i < NUM_PARTICLES; i++) {
                particles[i * 2] = Math.random() * 2 - 1;
                particles[i * 2 + 1] = Math.random() * 2 - 1;
                velocities[i * 2] = (Math.random() - 0.5) * 0.01;
                velocities[i * 2 + 1] = (Math.random() - 0.5) * 0.01;
                sizes[i] = Math.random() * 10 + 2;
                colors[i * 3] = Math.random();
                colors[i * 3 + 1] = Math.random();
                colors[i * 3 + 2] = Math.random();
            }
        }

        initParticles();

        // Create buffers
        const positionBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, particles, gl.DYNAMIC_DRAW);

        const sizeBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);

        const colorBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);

        // Get attribute locations
        const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position');
        const sizeAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_size');
        const colorAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_color');

        // Mouse interaction
        let mouseX = 0, mouseY = 0;
        canvas.addEventListener('mousemove', (event) => {
            const rect = canvas.getBoundingClientRect();
            mouseX = (event.clientX - rect.left) / canvas.width * 2 - 1;
            mouseY = -((event.clientY - rect.top) / canvas.height * 2 - 1);
        });

        // Animation loop
        function draw(now) {
            resizeCanvasToDisplaySize(gl.canvas);
            gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

            gl.clearColor(0.0, 0.0, 0.0, 1.0);
            gl.clear(gl.COLOR_BUFFER_BIT);

            gl.useProgram(shaderProgram);

            // Update and draw particles
            for (let i = 0; i < NUM_PARTICLES; i++) {
                particles[i * 2] += velocities[i * 2];
                particles[i * 2 + 1] += velocities[i * 2 + 1];

                // Bounce off edges
                if (Math.abs(particles[i * 2]) > 1) velocities[i * 2] *= -1;
                if (Math.abs(particles[i * 2 + 1]) > 1) velocities[i * 2 + 1] *= -1;

                // Mouse attraction
                let dx = mouseX - particles[i * 2];
                let dy = mouseY - particles[i * 2 + 1];
                let dist = Math.sqrt(dx * dx + dy * dy);
                if (dist < 0.1) {
                    velocities[i * 2] += dx * 0.0001 / dist;
                    velocities[i * 2 + 1] += dy * 0.0001 / dist;
                }

                // Color transition
                colors[i * 3] = (Math.sin(now * 0.001 + i * 0.1) + 1) * 0.5;
                colors[i * 3 + 1] = (Math.sin(now * 0.002 + i * 0.1) + 1) * 0.5;
                colors[i * 3 + 2] = (Math.sin(now * 0.003 + i * 0.1) + 1) * 0.5;
            }

            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.bufferSubData(gl.ARRAY_BUFFER, 0, particles);

            gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
            gl.bufferSubData(gl.ARRAY_BUFFER, 0, colors);

            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(positionAttributeLocation);

            gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
            gl.vertexAttribPointer(sizeAttributeLocation, 1, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(sizeAttributeLocation);

            gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
            gl.vertexAttribPointer(colorAttributeLocation, 3, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(colorAttributeLocation);

            gl.drawArrays(gl.POINTS, 0, NUM_PARTICLES);

            requestAnimationFrame(draw);
        }

        function resizeCanvasToDisplaySize(canvas) {
            const displayWidth  = canvas.clientWidth;
            const displayHeight = canvas.clientHeight;

            if (canvas.width  !== displayWidth ||
                canvas.height !== displayHeight) {
                canvas.width  = displayWidth;
                canvas.height = displayHeight;
            }
        }

        // Start the animation
        requestAnimationFrame(draw);
    </script>
</body>
</html>
공유하기
카카오로 공유하기
페이스북 공유하기
트위터로 공유하기
url 복사하기
조회수 : 354
heart
T
페이지 기반 대답
AI Chat