WebGL로 만드는 아티스틱 파티클 시뮬레이터
요약
- WebGL을 사용하여 아름답고 인터랙티브한 파티클 시뮬레이터를 만드는 방법을 소개합니다.
- 쉐이더 프로그램, 파티클 시스템 설정, 렌더링 및 인터랙티브 기능 추가의 단계별 설명을 제공합니다.
- 전체 코드와 함께 마우스 인터랙션을 추가하여 파티클이 마우스 커서를 따라가도록 하는 방법을 설명합니다.
WebGL로 만드는 아티스틱 파티클 시뮬레이터
안녕하세요! 오늘은 WebGL을 사용하여 아름답고 인터랙티브한 파티클 시뮬레이터를 만드는 방법을 알아보겠습니다. 이 튜토리얼은 WebGL 초보자를 위해 작성되었으며, 단계별로 코드를 설명할 것입니다.
목차
소개
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에서는 쉐이더를 사용하여 그래픽을 렌더링합니다. 우리는 두 가지 쉐이더를 만들 것입니다:
버텍스 쉐이더: 파티클의 위치와 크기를 결정합니다.
프래그먼트 쉐이더: 파티클의 색상과 모양을 결정합니다.
// 버텍스 쉐이더
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>
공유하기
조회수 : 380