3차원 그래픽에서 특수 효과는 쉐이더를 통해 대부분 구현됩니다. 이 글은 간단한 GLSL 쉐이더 코드를 통해 물과 불에 대한 효과를 소개합니다.
먼저 불에 대해 구현하고자 하는 모습은 다음과 같습니다.
다음은 물에 대한 결과입니다.
전체 소스코드는 아래 링크를 통해 다운로드 받으실 수 있습니다. 웹기반에서 구현된 코드이므로 js와 css, html 파일로 구성되어 있으며 WebGL 2.0으로 쉐이더 코드가 실행됩니다.

공간정보시스템 / 3차원 시각화 / 딥러닝 기반 기술 연구소 @지오서비스(GEOSERVICE)
MIT 라이선스인 chart.js를 예제를 통해 간단하게 정리해 봅니다. 구현하고자 하는 모습은 아래의 영상과 같습니다.
꺽은선 차트이며, 2개의 데이터 축을 가지고 1초마다 데이터가 변경됩니다.
먼저 DOM 구조입니다.
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div>
<canvas id="chart"></canvas>
</div>
CSS는 다음과 같구요.
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap');
body {
margin: 0;
padding: 0;
}
div {
width: 600px;
height: 400px;
padding: 20px;
}
이제 JS 코드에 대한 내용인데, 먼저 차트를 위한 기본 골격은 다음과 같습니다.
Chart.defaults.font.family = 'Noto Sans KR';
const ctx = document.getElementById('chart');
const config = {
// CORE PART !!
};
const chart = new Chart(ctx, config);
위의 config에 속성값을 지정함으로써 차트를 정의하는 세세한 내용을 결정할 수 있습니다. config에 대한 속성값은 다음과 같이 정의했습니다.
const config = {
type: 'line',
data: {
labels: ['빨강', 'Blue', 'Yellow', '초록', 'Purple', '오렌지'],
datasets: [
{
label: '변곡점1',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: 'yellow',
borderColor: 'black',
borderWidth: 1
},
{
label: '변곡점2',
data: [10, 16, 7, 6, 4, 2],
backgroundColor: 'white',
borderColor: 'gray',
borderWidth: 1
}
]
},
options: {
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '차트, 그것이 알고 싶다.'
}
},
scales: {
x: {
title: {
display: true,
text: '색상'
}
},
y: {
title: {
display: true,
text: '변곡량'
}
}
},
}
}
앞서 실행 화면을 보면 1초마다 차트의 값이 변경되는 것을 볼 수 있습니다. 이에 대한 코드는 다음과 같습니다.
setInterval(() => {
const datasets = config.data.datasets;
for (let iDataset = 0; iDataset < datasets.length; iDataset++) {
const data = datasets[iDataset].data;
for (let iValue = 0; iValue < data.length; iValue++) {
data[iValue] += Math.random() * 2.0 - 1.0;
}
}
chart.update();
}, 1000);
자바스크립트에서 데이터 컨테이너로써의 관점에서 데이터를 순회하는 코드를 정리합니다.
먼저 Array입니다.
const arr = new Array(1, 2, 3, 4, 5)
console.log('Array Iteration 1')
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
console.log('Array Iteration 2')
arr.forEach(v => {
console.log(v)
})
console.log('Array Iteration 3')
for (let v of arr) {
console.log(v)
}
console.log('Array Iteration 4')
for (let i in arr) {
console.log(arr[i])
}
다음은 Set입니다.
const set = new Set([1, 2, 3, 4, 'Hello'])
console.log('Set Iteration 1')
for (let v of set) {
console.log(v)
}
console.log('Set Iteration 2')
for (let v of set.values()) {
console.log(v)
}
console.log('Set Iteration 3')
set.forEach(v => {
console.log(v)
});
다음은 Object입니다.
const obj = { a: 1, b: 2, c: 3, 9: 4, e: 'Hello' }
console.log('Object Iteration 1')
const keys = Object.keys(obj) // [ 'a', 'b', 'c', '9', 'e' ]
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const v = obj[k]
console.log(k, v)
}
console.log('Object Iteration 2')
const values = Object.values(obj) // [ 1, 2, 3, 4, 'Hello' ]
for (let i = 0; i < values.length; i++) {
const v = values[i]
console.log(v)
}
console.log('Object Iteration 3')
const entries = Object.entries(obj) // [ ['a', 1], ['b', 2], ['c', 3], ['9', 4], ['e', 'Hello'] ]
for (let i = 0; i < entries.length; i++) {
const k = entries[i][0]
const v = entries[i][1]
console.log(k, v)
}
console.log('Object Iteration 4')
for (let k in obj) {
const v = obj[k]
console.log(k, v)
}
끝으로 Map입니다.
const map = new Map( [ ['a', 1], ['b', 2], ['c', 3], ['9', 4], ['e', 'Hello'] ])
console.log('Map Iteration 1')
for (let [k, v] of map) {
console.log(k, v)
}
console.log('Map Iteration 2')
for (let k of map.keys()) {
console.log(k, map.get(k))
}
console.log('Map Iteration 3')
for (let v of map.values()) {
console.log(v)
}
console.log('Map Iteration 4')
for (let [k, v] of map.entries()) {
console.log(k, v)
}
console.log('Map Iteration 5')
map.forEach(function(v, k) {
console.log(k, v)
})
사실 Map의 출현으로 더 이상 Object를 데이터 컨테이너로써 사용하는 것은 옳지 않습니다. Object는 기본적으로 생성될 때 기본 키값을 갖지만 Map은 개발자가 추가하지 않은 데이터는 갖지 않습니다. 또한 Map의 Key는 문자열 객체 이외에도 타입도 가능합니다. 또한 저장한 순서대로 값을 얻을 수 있으며 데이터의 개수도 size 함수를 통해 바로 얻을 수 있습니다. 게다가 Map은 데이터의 추가와 삭제 시 Object보다 성능이 뛰어납니다.
matter.js 라이브러리를 이용하여 구현하고자 하는 결과는 아래와 같습니다.
먼저 HTML 코드입니다.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style_matter.js.css">
<script src="decomp.js"></script>
<script src="pathseg.js"></script>
<script src="matter.0.16.1.js"></script>
<script src="app_matter.js.js" defer></script>
</head>
<body>
<div></div>
</body>
</html>
지형은 SVG 데이터를 통해 좌표를 뽑아오는데 이를 위해 matter.js 이외에도 decomp.js와 pathseg.js 라이브러리가 필요합니다. div 요소에 강체 시뮬레이션의 결과가 표시됩니다. 다음은 CSS입니다.
* {
outline: none;
padding: 0;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
}
div 요소를 화면 중심에 놓기 위한 것이 전부입니다. 이제 js 코드를 살펴보겠습니다.
먼저 아래의 코드로 기본 코드를 작성합니다.
const engine = Matter.Engine.create();
const world = engine.world;
const render = Matter.Render.create({
element: document.querySelector("div"),
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false,
background: 'black'
}
});
Matter.Render.run(render);
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
다음으로 지형과 지형 속에 사각형 물체를 구성하는 코드입니다.
fetch("./terrain.svg")
.then((response) => { return response.text(); })
.then((raw) => { return (new window.DOMParser()).parseFromString(raw, "image/svg+xml"); })
.then(function(root) {
const paths = Array.prototype.slice.call(root.querySelectorAll('path'));
const vertices = paths.map((path) => { return Matter.Svg.pathToVertices(path, 30); });
const terrain = Matter.Bodies.fromVertices(400, 350, vertices, {
isStatic: true,
render: {
fillStyle: '#2c3e50',
strokeStyle: '#2c3e50',
lineWidth: 1,
}
}, true);
Matter.World.add(world, terrain);
const bodyOptions = {
frictionAir: 0.1,
friction: 0.5,
restitution: 0.1
};
Matter.World.add(world, Matter.Composites.stack(100, 200, 40, 10, 15, 15, (x, y) => {
if (Matter.Query.point([terrain], { x: x, y: y }).length === 0) {
return Matter.Bodies.polygon(x, y, 4, 10, bodyOptions);
}
}));
}
);
끝으로 마우스를 통해 사각형 객체를 드레그하여 옮길 수 있도록 합니다.
const mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false
}
}
});
Matter.World.add(world, mouseConstraint);
matter.js는 매우 정교한 강체 시뮬레이션에는 적합하지 않으나 시각적인 면에서 다양한 물리 효과를 2차원에서 폭넓게 응용할 수 있다는 점이 매우 큰 장점입니다.
아래는 필요한 코드와 데이터 전체를 다운로드 받을 수 있는 링크입니다.
만약 다음과 같은 자바스크립트 객체가 있다고 할 때..
const obj = {
a: 'Dip2K',
b: 30,
c: true,
d: {}
};
obj 객체를 구성하는 전체 키를 배열로 얻는 코드는 다음과 같습니다.
console.log(Object.keys(obj)); // Array ["a", "b", "c", "d"]
다시 obj 객체를 구성하는 전체 값을 배열로 얻는 코드는 다음과 같구요.
console.log(Object.values(obj));
// Array ["Dip2K", 30, true, Object { }]
배열에 대해서 각 구성 항목을 참조하는 코드는 다음과 같습니다.
for(i of Object.values(obj)) {
console.log(i);
}
/*
"Dip2K"
30
true
Object { }
*/