
Blue Undulating Sphere/
โโโ index.html # ์บ๋ฒ์ค + main.js ๋ก๋
โโโ style.css # ์บ๋ฒ์ค ์คํ์ผ
โโโ main.js # ์
ฐ์ด๋ fetch, WebGL ์ด๊ธฐํ, ๋ฒํผ ์์ฑ, ๋ ๋ ๋ฃจํ, ํ๋ ฌ ์ํ
โโโ shaders/
โโโ vertex.glsl # Vertex Shader (์์ GLSL ์ฝ๋)
โโโ fragment.glsl # Fragment Shader (์์ GLSL ์ฝ๋)
์
ฐ์ด๋ GLSL ์ฝ๋๋ main.js ์์ ์๋ฐ์คํฌ๋ฆฝํธ ํ
ํ๋ฆฟ ๋ฌธ์์ด(`)๋ก ์ฝ์
๊ฐ๋ฅํฉ๋๋ค.
ํ์ง๋ง ์ด ๋ฐฉ์์ ์ฝ๋๊ฐ ๊ธธ์ด์ง๋ฉด ๋ก์ง๊ณผ ์
ฐ์ด๋๊ฐ ์์ฌ ์ฝ๊ธฐ ์ด๋ ต๊ณ , ์๋ํฐ์ GLSL ๋ฌธ๋ฒ ํ์ด๋ผ์ดํธ/ํฌ๋งทํ
๋ ๋ฐ์ ์ ์์ต๋๋ค. ๊ทธ๋์ ์์ .glsl ํ์ผ๋ก ๋ถ๋ฆฌํ๋๊ฒ ์ข์ต๋๋ค.
fetch()๋ก ๋น๋๊ธฐ ๋ก๋.glsl ํ์ผ์ ๊ทธ๋ฅ ํ
์คํธ ํ์ผ์
๋๋ค. ๋ธ๋ผ์ฐ์ ์ fetch()๋ก ์ฝ์ด์์ ์
ฐ์ด๋ ์์ค ๋ฌธ์์ด๋ก ์ฌ์ฉํฉ๋๋ค.
main.js์ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
async function main() {
const [vsSource, fsSource] = await Promise.all([
loadShaderSource("shaders/vertex.glsl"),
loadShaderSource("shaders/fragment.glsl"),
]);
initWebGL(vsSource, fsSource); // ๋ก๋ ์๋ฃ ํ ์ด๊ธฐํ
}
async function loadShaderSource(url) {
const response = await fetch(url);
if (!response.ok)
throw new Error(`${url} ๋ก๋ ์คํจ (HTTP ${response.status})`);
return response.text();
}
await๋ก ๊ธฐ๋ค๋ฆฐ ๋คinitWebGL()์ ํธ์ถํฉ๋๋ค. ๊ทธ๋์ initWebGL์ ์
ฐ์ด๋ ๋ฌธ์์ด์ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ต๋๋ค.Promise.all: ๋ ์
ฐ์ด๋๋ฅผ ๋์์(๋ณ๋ ฌ) ์์ฒญํด ๋ ๋นจ๋ฆฌ ๋ฐ์ต๋๋ค.response.ok๋ฅผ ํ์ธํ๊ณ , ์คํจ ์ try/catch๋ก ์ฝ์์ ์ถ๋ ฅํฉ๋๋ค.fetch()๋ ๋ณด์ ์ ์ฑ
(CORS) ๋๋ฌธ์ file://๋ก HTML์ ๋๋ธํด๋ฆญํด์ ์ด๋ฉด ๋์ํ์ง ์์ต๋๋ค. ๋ฐ๋์ VS Code์ Live Server ๋ฑ ๋ก์ปฌ ์๋ฒ์ ๋์์ผ ํฉ๋๋ค.
# ํ๋ก์ ํธ ํด๋์์ (Python 3)
python3 -m http.server 8000
# ๋ธ๋ผ์ฐ์ ์์ http://localhost:8000 ์ ์
์ ์ ๋ฐ์ดํฐ(JS) โ VBO/VAO โ Vertex Shader โ ๋์คํฐํ โ Fragment Shader โ ํ๋ฉด
generateSphere() (์ ์ ๋ง๋ค) (ํฝ์
๋ง๋ค)
generateSphere() ๊ฐ CPU์์ ๊ตฌ์ ์ ์ /๋ฒ์ /UV/์ธ๋ฑ์ค ์์ฑ.v_* ๊ฐ๋ค์ ๋ณด๊ฐ.generateSphere(radius, lat, lon)์๋(lat) / ๊ฒฝ๋(lon)๋ฅผ ๋ฐ๋ผ ๊ตฌ๋ฉด ์ขํ๋ฅผ ์ง๊ต ์ขํ๋ก ๋ณํํฉ๋๋ค.
x = cosPhi * sinTheta;
y = cosTheta;
z = sinPhi * sinTheta;
(0,0,0)์ด๋ผ ์ ์ ์์น = ๋ฒ์ ๋ฐฉํฅ์
๋๋ค.normals.push(x, y, z)๋ก ์์น๋ฅผ ๊ทธ๋๋ก ๋ฒ์ ์ผ๋ก ์๋๋ค.(j/lon, i/lat) โ 0 ~ 1 ๋ฒ์. ํ
์ค์ฒ ๋งคํ/์ ์ฐจ์ ํจํด์ ์ฌ์ฉ.indices๋ Uint16Array(16๋นํธ)์ด๋ฏ๋ก ์ต๋ ์ ์ ์๋ 65,536๊ฐ์
๋๋ค.128 x 128 ์ด๋ผ ์ฝ 16,641๊ฐ ์ ์ ์ผ๋ก ์์ ํฉ๋๋ค. ๊ฒฉ์๋ฅผ ๋ ํค์ฐ๋ฉด 16๋นํธ ์ธ๋ฑ์ค ํ๊ณ๋ฅผ ๋์ด ๊นจ์ง ์ ์์ผ๋ ์ฃผ์ํ์ธ์.bindVertexArray ํ ๋ฒ์ด๋ฉด ๋จ.vertexAttribPointer(index, size, type, normalized, stride, offset)์
index(0,1,2)๋ ์
ฐ์ด๋์ layout(location = N)๊ณผ ์ผ์นํด์ผ ํฉ๋๋ค.
| location | attribute | size |
|---|---|---|
| 0 | a_position | 3 |
| 1 | a_normal | 3 |
| 2 | a_uv | 2 |
shaders/vertex.glsl)float displacement = sin(a_position.x * 5.0 + u_time) *
sin(a_position.y * 5.0 + u_time) *
sin(a_position.z * 5.0 + u_time) * 0.3;
vec3 displacedPosition = a_position + normalize(a_position) * displacement;
u_time์ด ๋งค ํ๋ ์ ์ฆ๊ฐ โ sine ๊ฐ์ด ๋ณํจ โ ํ๋ฉด์ด ์ถ๋ ์ด๋ ์ ๋๋ฉ์ด์
.normalize(a_position)๋ ๋ฐ๊นฅ(๋ฒ์ ) ๋ฐฉํฅ. ๊ทธ ๋ฐฉํฅ์ผ๋ก ๋ฐ์ด๋ด ๊ตฌ๊ฐ ๋ถํ์๋ค ์ค์๋ค ํจ.gl_Position = u_projection * u_view * u_model * vec4(displacedPosition, 1.0);
v_normal = mat3(u_model) * a_normal;shaders/fragment.glsl) โ Phong ์กฐ๋ช
Phong ๋ชจ๋ธ์ ์ธ ๊ฐ์ง ๋น์ ํฉ์ ๋๋ค.
์ต์ข
์ = Ambient(ํ๊ฒฝ๊ด) + Diffuse(๋๋ฐ์ฌ) + Specular(์ ๋ฐ์ฌ)
| ํญ๋ชฉ | ์๋ฏธ | ์ฝ๋ ํต์ฌ |
|---|---|---|
| Ambient | ์ด๋์๋ ๋ฐ๋ ์ต์ํ์ ๋น | 0.2 * baseColor |
| Diffuse | ํ๋ฉด์ด ๋น์ ํฅํ ์ ๋ | max(dot(normal, lightDir), 0.3) |
| Specular | ๋ฐ์ง์ด๋ ํ์ด๋ผ์ดํธ(๊ดํ) | pow(max(dot(viewDir, reflectDir),0), 16) |
normalize๋ ํ์: ๋ณด๊ฐ๋ v_normal์ ๊ธธ์ด๊ฐ 1์ด ์๋ ์ ์์ด ๋ค์ ์ ๊ทํ.dot(normal, lightDir): ๋ ๋ฒกํฐ๊ฐ ๊ฐ์ ๋ฐฉํฅ์ผ์๋ก 1์ ๊ฐ๊น๊ณ ๋ฐ์์ง.0.3์ผ๋ก ํํ์ ๋ฌ์ ๊ทธ๋ฆผ์ ๋ถ๋ถ์ด ์์ ํ ๊ฒ๊ฒ ๋์ง ์๊ฒ ํจ(์คํ์ผ ์ ํ).reflect(-lightDir, normal): ๋น์ด ํ๋ฉด์์ ๋ฐ์ฌ๋๋ ๋ฐฉํฅ ๊ณ์ฐ.main.js ํ๋จ)perspective(fovy, aspect, near, far): ์๊ทผ ํฌ์. ๋ฉ๋ฆฌ ์๋ ๊ฒ์ด ์๊ฒ ๋ณด์ด๊ฒ ํจ.aspect = canvas.width / canvas.height (ํ์ฌ 1000x1000์ด๋ผ 1.0).lookAt(eye, target, up): ์นด๋ฉ๋ผ๋ฅผ eye์ ๋๊ณ target์ ๋ฐ๋ผ๋ณด๋ ๋ทฐ ํ๋ ฌ.eye = [0,0,5]๋ก z์ถ +5์์ ์์ ์ ๋ฐ๋ผ๋ด
๋๋ค.requestAnimationFramefunction render(time) {
time *= 0.001; // ๋ฐ๋ฆฌ์ด โ ์ด
gl.clear(...);
gl.uniform1f(locs.time, time); // ์
ฐ์ด๋์ ์๊ฐ ์ ๋ฌ โ ์ ๋๋ฉ์ด์
gl.drawElements(...);
requestAnimationFrame(render); // ๋ค์ ํ๋ ์ ์์ฝ
}
time์ ์
ฐ์ด๋์ u_time์ผ๋ก ์ ๋ฌ๋์ด ํ๋ฉด ์ถ๋ ์์ ๋ง๋ญ๋๋ค.gl.enable(gl.DEPTH_TEST)๋ก ๊น์ด ํ
์คํธ๋ฅผ ์ผ์ ๋ค์ชฝ ๋ฉด์ด ์์ ๋ฎ์ง ์๊ฒ ํจ.fragment.glsl์์ shininess๋ฅผ 4.0, 64.0์ผ๋ก ๋ฐ๊ฟ ๊ดํ ๋ณํ ๊ด์ฐฐ.vertex.glsl์ * 0.3 (๋ณํ ์ธ๊ธฐ)์ 0.0์ผ๋ก ํ๋ฉด ์์ ํ ๊ตฌ๊ฐ ๋จ.main.js์ gl.uniform3f(locs.baseColor, ...)๋ก ๊ตฌ์ ์์ ๋ฐ๊ฟ ๋ณด๊ธฐ.generateSphere(1.3, 16, 16)์ฒ๋ผ ๋ถํ ์๋ฅผ ์ค์ฌ ํด๋ฆฌ๊ณค์ด ๋ณด์ด๊ฒ ํด๋ณด๊ธฐ.u_lightDirection ๊ฐ์ ๋ฐ๊ฟ ์กฐ๋ช
๋ฐฉํฅ์ ๋ฐ๋ฅธ ์์ ๋ณํ ๊ด์ฐฐ.์๋์ ์ผ๋ก ๋จ์ํํ ๋ถ๋ถ๋ค์ ๋๋ค. ๋ ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด:
createShader/createProgram์์getShaderParameter(..., COMPILE_STATUS)์ getProgramParameter(..., LINK_STATUS)๋กgetShaderInfoLog๋ก ์๋ฌ๋ฅผ ์ถ๋ ฅํ๋ ๊ฒ์ด ์ ์์
๋๋ค.<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGL2 Modern Point Rendering</title>
<link rel="stylesheet" href="style.css" />
<!--WebGL ๋ก์ง์ ๋ด์ ์คํฌ๋ฆฝํธ ํ์ผ (์
ฐ์ด๋๋ fetch๋ก ๋น๋๊ธฐ ๋ก๋)-->
<script src="main.js" defer></script>
</head>
<body>
<!--WebGL ๋ ๋๋ง์ ์ํ ์บ๋ฒ์ค-->
<canvas id="webgl_canvas" width="1000" height="1000"></canvas>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ์ ์ฒด ํ์ด์ง ๋ ์ด์์ ์ค์ */
body {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center; /* ๊ฐ๋ก ์ค์ ์ ๋ ฌ */
align-items: center; /* ์ธ๋ก ์ค์ ์ ๋ ฌ */
background-color: #1a1a1a;
overflow: hidden; /* ์คํฌ๋กค๋ฐ ๋ฐฉ์ง */
}
/* ์บ๋ฒ์ค ์คํ์ผ */
#webgl_canvas {
display: block;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
border: 1px solid #333;
background-color: #000;
}
/**
* ============================================================================
* [WebGL2 Procedural Animated Sphere]
* - ๊ธฐ๋ฅ: Sine ํจ์๋ฅผ ์ด์ฉํ ํ๋ฉด ๋ณํ(Wave), Phong Lighting, Blue Glossy Material
* - ๋ฐฉ์: ์ ์ ๋ฐ์ดํฐ ์์ฑ ์ UV ์ขํ๋ฅผ ํฌํจํ์ฌ ์ํ์ ๋ณํ ์ ๋
* ============================================================================
*/
const canvas = document.getElementById("webgl_canvas");
const gl = canvas.getContext("webgl2");
if (!gl) {
console.error("WebGL2๋ฅผ ์ง์ํ์ง ์์ต๋๋ค.");
} else {
// ์
ฐ์ด๋(.glsl) ํ์ผ์ fetch๋ก ๋น๋๊ธฐ ๋ก๋ํ ๋ค ์ด๊ธฐํํ๋ค.
main();
}
// --- [0. ์
ฐ์ด๋ ํ์ผ ๋ก๋ ํ ์ด๊ธฐํ ์์] ---
async function main() {
try {
const [vsSource, fsSource] = await Promise.all([
loadShaderSource("shaders/vertex.glsl"),
loadShaderSource("shaders/fragment.glsl"),
]);
initWebGL(vsSource, fsSource);
} catch (err) {
console.error("์
ฐ์ด๋ ๋ก๋ ์คํจ:", err);
}
}
// ์
ฐ์ด๋ ์์ค ํ์ผ(.glsl)์ ํ
์คํธ๋ก ์ฝ์ด์จ๋ค.
async function loadShaderSource(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${url} ๋ก๋ ์คํจ (HTTP ${response.status})`);
}
return response.text();
}
function initWebGL(vsSource, fsSource) {
// --- [1. ์
ฐ์ด๋ ์์ค ์ ์] ---
// ์
ฐ์ด๋ ์ฝ๋๋ shaders/ ํด๋์ .glsl ํ์ผ๋ก ๋ถ๋ฆฌ๋์ด ์์ผ๋ฉฐ,
// main()์์ fetch๋ก ์ฝ์ด์ ๋งค๊ฐ๋ณ์(vsSource, fsSource)๋ก ์ ๋ฌ๋ฐ๋๋ค.
// - shaders/vertex.glsl (Vertex Shader)
// - shaders/fragment.glsl (Fragment Shader)
// --- [2. ์
ฐ์ด๋ ์ปดํ์ผ ๋ฐ ํ๋ก๊ทธ๋จ ์์ฑ] ---
const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);
// --- [3. ๊ตฌ(Sphere) ๋ฐ์ดํฐ ์์ฑ (์ ์ฐจ์ ์์ฑ)] ---
const { vertices, normals, uvs, indices } = generateSphere(1.3, 128, 128);
// VAO ์ค์
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// VBO 1: Positions
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
// VBO 2: Normals
const normBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
// VBO 3: UVs
const uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvs, gl.STATIC_DRAW);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 0, 0);
// EBO: Indices (Element Buffer Object)
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices),
gl.STATIC_DRAW,
);
// --- [4. Uniform ์์น ์ฐพ๊ธฐ] ---
const locs = {
projection: gl.getUniformLocation(program, "u_projection"),
view: gl.getUniformLocation(program, "u_view"),
model: gl.getUniformLocation(program, "u_model"),
time: gl.getUniformLocation(program, "u_time"),
lightDir: gl.getUniformLocation(program, "u_lightDirection"),
viewPos: gl.getUniformLocation(program, "u_viewPos"),
baseColor: gl.getUniformLocation(program, "u_baseColor"),
};
// --- [5. ํ๋ ฌ ๋ฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (๊ฐ์ด ๊ตฌํ)] ---
// ์ค์ ํ๋ก์ ํธ์์๋ gl-matrix ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
const projectionMatrix = perspective(
(45 * Math.PI) / 180,
canvas.width / canvas.height,
0.1,
100.0,
);
const viewMatrix = lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]);
// --- [6. ๋ ๋๋ง ๋ฃจํ] ---
gl.clearColor(1.0, 1.0, 1.0, 1.0); // ๋ฐฐ๊ฒฝ์: ํ์ดํธ
gl.enable(gl.DEPTH_TEST); // ๊น์ด ํ
์คํธ ํ์ฑํ
function render(time) {
time *= 0.001; // ์ด ๋จ์ ๋ณํ
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.uniformMatrix4fv(locs.projection, false, projectionMatrix);
gl.uniformMatrix4fv(locs.view, false, viewMatrix);
// ๋ชจ๋ธ ํ๋ ฌ: ๊ธฐ๋ณธ ์ ์ฒด ํ๋ ฌ์์ ์์
const modelMatrix = mat4Identity();
gl.uniformMatrix4fv(locs.model, false, modelMatrix);
// Uniform ๊ฐ ์
๋ฐ์ดํธ
gl.uniform1f(locs.time, time);
gl.uniform3f(locs.lightDir, 1.0, 2.0, 1.0); // ์กฐ๋ช
๋ฐฉํฅ
gl.uniform3f(locs.viewPos, 0, 0, 5); // ์นด๋ฉ๋ผ ์์น
gl.uniform3f(locs.baseColor, 0.0, 0.4, 1.0); // ๊ตฌ ์์: ํ๋์
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
// --- [ํฌํผ ํจ์๋ค] ---
function generateSphere(radius, lat, lon) {
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
for (let i = 0; i <= lat; i++) {
const theta = (i * Math.PI) / lat;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let j = 0; j <= lon; j++) {
const phi = (j * 2 * Math.PI) / lon;
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const x = cosPhi * sinTheta;
const y = cosTheta;
const z = sinPhi * sinTheta;
vertices.push(x * radius, y * radius, z * radius);
normals.push(x, y, z); // ๊ตฌ์ ์ค์ฌ์ด (0,0,0)์ด๋ฏ๋ก ์ ์ ์์น๊ฐ ๊ณง ๋ฒ์
uvs.push(j / lon, i / lat);
}
}
for (let i = 0; i < lat; i++) {
for (let j = 0; j < lon; j++) {
const first = i * (lon + 1) + j;
const second = first + lon + 1;
indices.push(first, second, first + 1);
indices.push(second, second + 1, first + 1);
}
}
return {
vertices: new Float32Array(vertices),
normals: new Float32Array(normals),
uvs: new Float32Array(uvs),
indices,
};
}
function createProgram(gl, vs, fs) {
const vShader = createShader(gl, gl.VERTEX_SHADER, vs);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fs);
const prog = gl.createProgram();
gl.attachShader(prog, vShader);
gl.attachShader(prog, fShader);
gl.linkProgram(prog);
return prog;
}
function createShader(gl, type, source) {
const s = gl.createShader(type);
gl.shaderSource(s, source);
gl.compileShader(s);
return s;
}
// --- [์ต์ํ์ ํ๋ ฌ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ] ---
function mat4Identity() {
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
}
function perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
return new Float32Array([
f / aspect,
0,
0,
0,
0,
f,
0,
0,
0,
0,
(far + near) * nf,
-1,
0,
0,
2 * far * near * nf,
0,
]);
}
function lookAt(eye, target, up) {
const z = normalize(subtract(eye, target));
const x = normalize(cross(up, z));
const y = normalize(cross(z, x));
return new Float32Array([
x[0],
y[0],
z[0],
0,
x[1],
y[1],
z[1],
0,
x[2],
y[2],
z[2],
0,
-dot(x, eye),
-dot(y, eye),
-dot(z, eye),
1,
]);
}
// ๋ฒกํฐ ์ฐ์ฐ ์ ํธ๋ฆฌํฐ
function normalize(v) {
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / len, v[1] / len, v[2] / len];
}
function subtract(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
function cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
/**
* ============================================================================
* [WebGL2 Procedural Animated Sphere]
* - ๊ธฐ๋ฅ: Sine ํจ์๋ฅผ ์ด์ฉํ ํ๋ฉด ๋ณํ(Wave), Phong Lighting, Blue Glossy Material
* - ๋ฐฉ์: ์ ์ ๋ฐ์ดํฐ ์์ฑ ์ UV ์ขํ๋ฅผ ํฌํจํ์ฌ ์ํ์ ๋ณํ ์ ๋
* ============================================================================
*/
const canvas = document.getElementById("webgl_canvas");
const gl = canvas.getContext("webgl2");
if (!gl) {
console.error("WebGL2๋ฅผ ์ง์ํ์ง ์์ต๋๋ค.");
} else {
// ์
ฐ์ด๋(.glsl) ํ์ผ์ fetch๋ก ๋น๋๊ธฐ ๋ก๋ํ ๋ค ์ด๊ธฐํํ๋ค.
main();
}
// --- [0. ์
ฐ์ด๋ ํ์ผ ๋ก๋ ํ ์ด๊ธฐํ ์์] ---
async function main() {
try {
const [vsSource, fsSource] = await Promise.all([
loadShaderSource("shaders/vertex.glsl"),
loadShaderSource("shaders/fragment.glsl"),
]);
initWebGL(vsSource, fsSource);
} catch (err) {
console.error("์
ฐ์ด๋ ๋ก๋ ์คํจ:", err);
}
}
// ์
ฐ์ด๋ ์์ค ํ์ผ(.glsl)์ ํ
์คํธ๋ก ์ฝ์ด์จ๋ค.
async function loadShaderSource(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${url} ๋ก๋ ์คํจ (HTTP ${response.status})`);
}
return response.text();
}
function initWebGL(vsSource, fsSource) {
// --- [1. ์
ฐ์ด๋ ์์ค ์ ์] ---
// ์
ฐ์ด๋ ์ฝ๋๋ shaders/ ํด๋์ .glsl ํ์ผ๋ก ๋ถ๋ฆฌ๋์ด ์์ผ๋ฉฐ,
// main()์์ fetch๋ก ์ฝ์ด์ ๋งค๊ฐ๋ณ์(vsSource, fsSource)๋ก ์ ๋ฌ๋ฐ๋๋ค.
// - shaders/vertex.glsl (Vertex Shader)
// - shaders/fragment.glsl (Fragment Shader)
// --- [2. ์
ฐ์ด๋ ์ปดํ์ผ ๋ฐ ํ๋ก๊ทธ๋จ ์์ฑ] ---
const program = createProgram(gl, vsSource, fsSource);
gl.useProgram(program);
// --- [3. ๊ตฌ(Sphere) ๋ฐ์ดํฐ ์์ฑ (์ ์ฐจ์ ์์ฑ)] ---
const { vertices, normals, uvs, indices } = generateSphere(1.3, 128, 128);
// VAO ์ค์
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// VBO 1: Positions
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
// VBO 2: Normals
const normBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.bufferData(gl.ARRAY_BUFFER, normals, gl.STATIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
// VBO 3: UVs
const uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, uvs, gl.STATIC_DRAW);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 0, 0);
// EBO: Indices (Element Buffer Object)
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint16Array(indices),
gl.STATIC_DRAW,
);
// --- [4. Uniform ์์น ์ฐพ๊ธฐ] ---
const locs = {
projection: gl.getUniformLocation(program, "u_projection"),
view: gl.getUniformLocation(program, "u_view"),
model: gl.getUniformLocation(program, "u_model"),
time: gl.getUniformLocation(program, "u_time"),
lightDir: gl.getUniformLocation(program, "u_lightDirection"),
viewPos: gl.getUniformLocation(program, "u_viewPos"),
baseColor: gl.getUniformLocation(program, "u_baseColor"),
};
// --- [5. ํ๋ ฌ ๋ฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ (๊ฐ์ด ๊ตฌํ)] ---
// ์ค์ ํ๋ก์ ํธ์์๋ gl-matrix ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
const projectionMatrix = perspective(
(45 * Math.PI) / 180,
canvas.width / canvas.height,
0.1,
100.0,
);
const viewMatrix = lookAt([0, 0, 5], [0, 0, 0], [0, 1, 0]);
// --- [6. ๋ ๋๋ง ๋ฃจํ] ---
gl.clearColor(1.0, 1.0, 1.0, 1.0); // ๋ฐฐ๊ฒฝ์: ํ์ดํธ
gl.enable(gl.DEPTH_TEST); // ๊น์ด ํ
์คํธ ํ์ฑํ
function render(time) {
time *= 0.001; // ์ด ๋จ์ ๋ณํ
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.uniformMatrix4fv(locs.projection, false, projectionMatrix);
gl.uniformMatrix4fv(locs.view, false, viewMatrix);
// ๋ชจ๋ธ ํ๋ ฌ: ๊ธฐ๋ณธ ์ ์ฒด ํ๋ ฌ์์ ์์
const modelMatrix = mat4Identity();
gl.uniformMatrix4fv(locs.model, false, modelMatrix);
// Uniform ๊ฐ ์
๋ฐ์ดํธ
gl.uniform1f(locs.time, time);
gl.uniform3f(locs.lightDir, 1.0, 2.0, 1.0); // ์กฐ๋ช
๋ฐฉํฅ
gl.uniform3f(locs.viewPos, 0, 0, 5); // ์นด๋ฉ๋ผ ์์น
gl.uniform3f(locs.baseColor, 0.0, 0.4, 1.0); // ๊ตฌ ์์: ํ๋์
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
// --- [ํฌํผ ํจ์๋ค] ---
function generateSphere(radius, lat, lon) {
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
for (let i = 0; i <= lat; i++) {
const theta = (i * Math.PI) / lat;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let j = 0; j <= lon; j++) {
const phi = (j * 2 * Math.PI) / lon;
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const x = cosPhi * sinTheta;
const y = cosTheta;
const z = sinPhi * sinTheta;
vertices.push(x * radius, y * radius, z * radius);
normals.push(x, y, z); // ๊ตฌ์ ์ค์ฌ์ด (0,0,0)์ด๋ฏ๋ก ์ ์ ์์น๊ฐ ๊ณง ๋ฒ์
uvs.push(j / lon, i / lat);
}
}
for (let i = 0; i < lat; i++) {
for (let j = 0; j < lon; j++) {
const first = i * (lon + 1) + j;
const second = first + lon + 1;
indices.push(first, second, first + 1);
indices.push(second, second + 1, first + 1);
}
}
return {
vertices: new Float32Array(vertices),
normals: new Float32Array(normals),
uvs: new Float32Array(uvs),
indices,
};
}
function createProgram(gl, vs, fs) {
const vShader = createShader(gl, gl.VERTEX_SHADER, vs);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fs);
const prog = gl.createProgram();
gl.attachShader(prog, vShader);
gl.attachShader(prog, fShader);
gl.linkProgram(prog);
return prog;
}
function createShader(gl, type, source) {
const s = gl.createShader(type);
gl.shaderSource(s, source);
gl.compileShader(s);
return s;
}
// --- [์ต์ํ์ ํ๋ ฌ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ] ---
function mat4Identity() {
return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
}
function perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
return new Float32Array([
f / aspect,
0,
0,
0,
0,
f,
0,
0,
0,
0,
(far + near) * nf,
-1,
0,
0,
2 * far * near * nf,
0,
]);
}
function lookAt(eye, target, up) {
const z = normalize(subtract(eye, target));
const x = normalize(cross(up, z));
const y = normalize(cross(z, x));
return new Float32Array([
x[0],
y[0],
z[0],
0,
x[1],
y[1],
z[1],
0,
x[2],
y[2],
z[2],
0,
-dot(x, eye),
-dot(y, eye),
-dot(z, eye),
1,
]);
}
// ๋ฒกํฐ ์ฐ์ฐ ์ ํธ๋ฆฌํฐ
function normalize(v) {
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
return [v[0] / len, v[1] / len, v[2] / len];
}
function subtract(a, b) {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
function cross(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_uv;
uniform mat4 u_projection;
uniform mat4 u_view;
uniform mat4 u_model;
uniform float u_time;
out vec3 v_normal;
out vec3 v_position;
out vec2 v_uv;
void main() {
// 1. ์ ์ฐจ์ ๋ณํ (Sine Wave): UV ์ขํ๋ฅผ ํ์ฉํด ๋ฐ์ง๋ฆ์ ์กฐ์
// ํ๋์ ๋์ด๋ฅผ ๊ณ์ฐํ์ฌ ์ ์ ์ ์์น๋ฅผ ๋ฏธ์ธํ๊ฒ ์ด๋์ํด
float displacement = sin(a_position.x * 5.0 + u_time) *
sin(a_position.y * 5.0 + u_time) *
sin(a_position.z * 5.0 + u_time) * 0.3;
vec3 displacedPosition = a_position + normalize(a_position) * displacement;
// 2. ์ขํ ๋ณํ
vec4 worldPosition = u_model * vec4(displacedPosition, 1.0);
v_position = worldPosition.xyz;
v_normal = mat3(u_model) * a_normal; // ๋ฒ์ ๋ฒกํฐ ๋ณํ
v_uv = a_uv;
gl_Position = u_projection * u_view * worldPosition;
}
#version 300 es
precision highp float;
in vec3 v_normal;
in vec3 v_position;
in vec2 v_uv;
uniform vec3 u_lightDirection;
uniform vec3 u_viewPos;
uniform vec3 u_baseColor;
out vec4 outColor;
void main() {
// ๊ธฐ๋ณธ ๋ฌผ๋ฆฌ๊ฐ ์ค์
vec3 normal = normalize(v_normal);
vec3 lightDir = normalize(u_lightDirection);
vec3 viewDir = normalize(u_viewPos - v_position);
vec3 reflectDir = reflect(-lightDir, normal);
// 1. Ambient (ํ๊ฒฝ๊ด)
float ambientStrength = 0.2;
vec3 ambient = ambientStrength * u_baseColor;
// 2. Diffuse (๋๋ฐ์ฌ)
float diff = max(dot(normal, lightDir), 0.3);
vec3 diffuse = diff * u_baseColor;
// 3. Specular (์ ๋ฐ์ฌ - Glossy ํจ๊ณผ)
float specularStrength = 0.8; // ๋ฐ์ฌ๊ด ๊ฐ๋
float shininess = 16.0; // ๊ดํ์ ๋ ์นด๋ก์
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
vec3 specular = specularStrength * spec * vec3(1.0, 1.0, 1.0);
// ์ต์ข
์์ ๊ฒฐํฉ
vec3 result = ambient + diffuse + specular;
outColor = vec4(result, 1.0);
}