๐Ÿชฉ Blue Undulating Sphere

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

WebGL

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

  • WebGL2๋กœ ํŒŒ๋ž€์ƒ‰ ๊ด‘ํƒ ็ƒ๋ฅผ ๊ทธ๋ฆฌ๊ณ ,
  • Sine ํ•จ์ˆ˜๋กœ ํ‘œ๋ฉด์„ ์ถœ๋ ์ด๊ฒŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋ฉฐ,
  • Phong ์กฐ๋ช… ๋ชจ๋ธ๋กœ ์Œ์˜์„ ์ž…ํžˆ๋Š” ์ฝ”๋“œ.

1. ํŒŒ์ผ ๊ตฌ์กฐ

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();
}
  • ๋น„๋™๊ธฐ(async)๊ฐ€ ํ•ต์‹ฌ: ํŒŒ์ผ์„ ๋ฐ›์•„์˜ค๋Š” ๋ฐ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๋ฏ€๋กœ 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 ์ ‘์†

2. ๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

์ •์  ๋ฐ์ดํ„ฐ(JS)  โ†’  VBO/VAO  โ†’  Vertex Shader  โ†’  ๋ž˜์Šคํ„ฐํ™”  โ†’  Fragment Shader  โ†’  ํ™”๋ฉด
generateSphere()                (์ •์ ๋งˆ๋‹ค)                      (ํ”ฝ์…€๋งˆ๋‹ค)
  1. generateSphere() ๊ฐ€ CPU์—์„œ ๊ตฌ์˜ ์ •์ /๋ฒ•์„ /UV/์ธ๋ฑ์Šค ์ƒ์„ฑ.
  2. ๋ฐ์ดํ„ฐ๋ฅผ VBO(GPU ๋ฒ„ํผ)์— ์˜ฌ๋ฆฌ๊ณ  VAO๋กœ ๋ฐ”์ธ๋”ฉ.
  3. Vertex Shader: ์ •์ ๋งˆ๋‹ค ์œ„์น˜๋ฅผ ๋ณ€ํ˜•ํ•˜๊ณ  ์ขŒํ‘œ๋ฅผ ๋ณ€ํ™˜.
  4. GPU๊ฐ€ ์‚ผ๊ฐํ˜•์„ ํ”ฝ์…€๋กœ ์ชผ๊ฐ ๋‹ค(๋ž˜์Šคํ„ฐํ™”)ํ•˜๋ฉฐ v_* ๊ฐ’๋“ค์„ ๋ณด๊ฐ„.
  5. Fragment Shader: ํ”ฝ์…€๋งˆ๋‹ค ์ƒ‰(์กฐ๋ช…)์„ ๊ณ„์‚ฐ.

3. ์ •์  ๋ฐ์ดํ„ฐ ์ƒ์„ฑ โ€” generateSphere(radius, lat, lon)

์œ„๋„(lat) / ๊ฒฝ๋„(lon)๋ฅผ ๋”ฐ๋ผ ๊ตฌ๋ฉด ์ขŒํ‘œ๋ฅผ ์ง๊ต ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

x = cosPhi * sinTheta;
y = cosTheta;
z = sinPhi * sinTheta;
  • ๋ฒ•์„ (normal): ๊ตฌ์˜ ์ค‘์‹ฌ์ด(0,0,0)์ด๋ผ ์ •์  ์œ„์น˜ = ๋ฒ•์„  ๋ฐฉํ–ฅ์ž…๋‹ˆ๋‹ค.
    ๊ทธ๋ž˜์„œ normals.push(x, y, z)๋กœ ์œ„์น˜๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฒ•์„ ์œผ๋กœ ์”๋‹ˆ๋‹ค.
  • UV ์ขŒํ‘œ: (j/lon, i/lat) โ†’ 0 ~ 1 ๋ฒ”์œ„. ํ…์Šค์ฒ˜ ๋งคํ•‘/์ ˆ์ฐจ์  ํŒจํ„ด์— ์‚ฌ์šฉ.
  • ์ธ๋ฑ์Šค(EBO): ์ •์ ์„ ์žฌ์‚ฌ์šฉํ•˜๋ ค๊ณ  ์‚ผ๊ฐํ˜• ๋‘ ๊ฐœ๋กœ ์‚ฌ๊ฐํ˜• ํ•˜๋‚˜๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    indices๋Š” Uint16Array(16๋น„ํŠธ)์ด๋ฏ€๋กœ ์ตœ๋Œ€ ์ •์  ์ˆ˜๋Š” 65,536๊ฐœ์ž…๋‹ˆ๋‹ค.
    ํ˜„์žฌ ์ฝ”๋“œ๋Š” 128 x 128 ์ด๋ผ ์•ฝ 16,641๊ฐœ ์ •์ ์œผ๋กœ ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฉ์ž๋ฅผ ๋” ํ‚ค์šฐ๋ฉด 16๋น„ํŠธ ์ธ๋ฑ์Šค ํ•œ๊ณ„๋ฅผ ๋„˜์–ด ๊นจ์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•˜์„ธ์š”.

4. ๋ฒ„ํผ ์„ค์ • โ€” VBO / VAO / EBO

  • VBO (Vertex Buffer Object): ์ •์  ์†์„ฑ(์œ„์น˜/๋ฒ•์„ /UV)์„ ๋‹ด๋Š” GPU ๋ฒ„ํผ.
  • VAO (Vertex Array Object): "์–ด๋–ค VBO๋ฅผ, ์–ด๋–ค attribute ์œ„์น˜๋กœ, ์–ด๋–ค ํ˜•์‹์œผ๋กœ ์ฝ์„์ง€"์— ๋Œ€ํ•œ ์„ค์ •์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด ์ €์žฅ. ๋ Œ๋” ๋•Œ bindVertexArray ํ•œ ๋ฒˆ์ด๋ฉด ๋จ.
  • EBO (Element/Index Buffer Object): ์ •์  ์ธ๋ฑ์Šค๋ฅผ ๋‹ด์•„ ์ •์  ์žฌ์‚ฌ์šฉ โ†’ ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ.

vertexAttribPointer(index, size, type, normalized, stride, offset)์˜
index(0,1,2)๋Š” ์…ฐ์ด๋”์˜ layout(location = N)๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

locationattributesize
0a_position3
1a_normal3
2a_uv2

5. Vertex Shader (shaders/vertex.glsl)

ํ•ต์‹ฌ 1) ์ ˆ์ฐจ์  ๋ณ€ํ˜• (Sine Wave Displacement)

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)๋Š” ๋ฐ”๊นฅ(๋ฒ•์„ ) ๋ฐฉํ–ฅ. ๊ทธ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฐ€์–ด๋‚ด ๊ตฌ๊ฐ€ ๋ถ€ํ’€์—ˆ๋‹ค ์ค„์—ˆ๋‹ค ํ•จ.

ํ•ต์‹ฌ 2) ์ขŒํ‘œ ๋ณ€ํ™˜ (MVP)

gl_Position = u_projection * u_view * u_model * vec4(displacedPosition, 1.0);
  • Model โ†’ View โ†’ Projection ์ˆœ์„œ๋กœ ๊ณฑํ•ด ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜.
  • GLSL ํ–‰๋ ฌ ๊ณฑ์€ ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค(model์ด ๋จผ์ €).

์ฃผ์˜ํ•  ์ 

  • v_normal = mat3(u_model) * a_normal;
    ์—ฌ๊ธฐ์„œ๋Š” ๋ชจ๋ธ ํ–‰๋ ฌ์ด ๋‹จ์œ„ํ–‰๋ ฌ(Identity)์ด๋ผ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋งŒ์•ฝ
    ๋น„๊ท ๋“ฑ ์Šค์ผ€์ผ(non-uniform scale)์„ ์ ์šฉํ•˜๋ฉด ๋ฒ•์„ ์ด ํ‹€์–ด์ง‘๋‹ˆ๋‹ค. ์ •์„์€
    normal matrix = transpose(inverse(model)) ๋ฅผ ์“ฐ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ์œ„์น˜๋Š” ๋ณ€ํ˜•(displacement)ํ–ˆ์ง€๋งŒ ๋ฒ•์„ ์€ ๋ณ€ํ˜• ์ „ ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์”๋‹ˆ๋‹ค.
    ๊ทธ๋ž˜์„œ ์กฐ๋ช…์ด 100% ์ •ํ™•ํ•˜์ง„ ์•Š์Šต๋‹ˆ๋‹ค(ํ•™์Šต์šฉ ๋‹จ์ˆœํ™”). ์ •ํ™•ํžˆ ํ•˜๋ ค๋ฉด ๋ณ€ํ˜•๋œ
    ํ‘œ๋ฉด์˜ ์ƒˆ ๋ฒ•์„ ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

6. Fragment Shader (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์— ๊ฐ€๊น๊ณ  ๋ฐ์•„์ง.
  • Diffuse์—์„œ 0.3์œผ๋กœ ํ•˜ํ•œ์„ ๋‘ฌ์„œ ๊ทธ๋ฆผ์ž ๋ถ€๋ถ„์ด ์™„์ „ํžˆ ๊ฒ€๊ฒŒ ๋˜์ง€ ์•Š๊ฒŒ ํ•จ(์Šคํƒ€์ผ ์„ ํƒ).
  • shininess(16.0): ๊ฐ’์ด ํด์ˆ˜๋ก ํ•˜์ด๋ผ์ดํŠธ๊ฐ€ ์ž‘๊ณ  ๋‚ ์นด๋กœ์›Œ์ง(๋” ๋งค๋ˆํ•œ ํ‘œ๋ฉด).
  • reflect(-lightDir, normal): ๋น›์ด ํ‘œ๋ฉด์—์„œ ๋ฐ˜์‚ฌ๋˜๋Š” ๋ฐฉํ–ฅ ๊ณ„์‚ฐ.

7. ํ–‰๋ ฌ / ์นด๋ฉ”๋ผ (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์—์„œ ์›์ ์„ ๋ฐ”๋ผ๋ด…๋‹ˆ๋‹ค.
  • ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ง์ ‘ ๊ตฌํ˜„ ๋Œ€์‹  gl-matrix ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

8. ๋ Œ๋” ๋ฃจํ”„ โ€” requestAnimationFrame

function render(time) {
  time *= 0.001;        // ๋ฐ€๋ฆฌ์ดˆ โ†’ ์ดˆ
  gl.clear(...);
  gl.uniform1f(locs.time, time);   // ์…ฐ์ด๋”์— ์‹œ๊ฐ„ ์ „๋‹ฌ โ†’ ์• ๋‹ˆ๋ฉ”์ด์…˜
  gl.drawElements(...);
  requestAnimationFrame(render);   // ๋‹ค์Œ ํ”„๋ ˆ์ž„ ์˜ˆ์•ฝ
}
  • ๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ ์ฃผ๊ธฐ(๋ณดํ†ต 60fps)์— ๋งž์ถฐ ๋งค ํ”„๋ ˆ์ž„ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • time์€ ์…ฐ์ด๋”์˜ u_time์œผ๋กœ ์ „๋‹ฌ๋˜์–ด ํ‘œ๋ฉด ์ถœ๋ ์ž„์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • gl.enable(gl.DEPTH_TEST)๋กœ ๊นŠ์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ผœ์„œ ๋’ค์ชฝ ๋ฉด์ด ์•ž์„ ๋ฎ์ง€ ์•Š๊ฒŒ ํ•จ.

9. ์•„ํŠธ์›Œํฌ ๋ณ€๊ฒฝ ํฌ์ธํŠธ

  1. fragment.glsl์—์„œ shininess๋ฅผ 4.0, 64.0์œผ๋กœ ๋ฐ”๊ฟ” ๊ด‘ํƒ ๋ณ€ํ™” ๊ด€์ฐฐ.
  2. vertex.glsl์˜ * 0.3 (๋ณ€ํ˜• ์„ธ๊ธฐ)์„ 0.0์œผ๋กœ ํ•˜๋ฉด ์™„์ „ํ•œ ๊ตฌ๊ฐ€ ๋จ.
  3. main.js์˜ gl.uniform3f(locs.baseColor, ...)๋กœ ๊ตฌ์˜ ์ƒ‰์„ ๋ฐ”๊ฟ” ๋ณด๊ธฐ.
  4. generateSphere(1.3, 16, 16)์ฒ˜๋Ÿผ ๋ถ„ํ•  ์ˆ˜๋ฅผ ์ค„์—ฌ ํด๋ฆฌ๊ณค์ด ๋ณด์ด๊ฒŒ ํ•ด๋ณด๊ธฐ.
  5. u_lightDirection ๊ฐ’์„ ๋ฐ”๊ฟ” ์กฐ๋ช… ๋ฐฉํ–ฅ์— ๋”ฐ๋ฅธ ์Œ์˜ ๋ณ€ํ™” ๊ด€์ฐฐ.

10. ์ด ์ฝ”๋“œ์˜ ๋‹จ์ˆœํ™” / ๊ฐœ์„  ์—ฌ์ง€

์˜๋„์ ์œผ๋กœ ๋‹จ์ˆœํ™”ํ•œ ๋ถ€๋ถ„๋“ค์ž…๋‹ˆ๋‹ค. ๋” ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด:

  • ์…ฐ์ด๋” ์ปดํŒŒ์ผ ์—๋Ÿฌ ์ฒดํฌ ์—†์Œ: createShader/createProgram์—์„œ
    getShaderParameter(..., COMPILE_STATUS)์™€ getProgramParameter(..., LINK_STATUS)๋กœ
    ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  getShaderInfoLog๋กœ ์—๋Ÿฌ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ๊ฒƒ์ด ์ •์„์ž…๋‹ˆ๋‹ค.
  • ๋ฒ•์„  ์žฌ๊ณ„์‚ฐ ์—†์Œ: 6์žฅ ์ฐธ๊ณ (๋ณ€ํ˜•๋œ ํ‘œ๋ฉด์˜ ๋ฒ•์„ ์„ ๋‹ค์‹œ ๊ตฌํ•˜์ง€ ์•Š์Œ).
  • normal matrix ๋ฏธ์ ์šฉ: 5์žฅ ์ฐธ๊ณ (๋น„๊ท ๋“ฑ ์Šค์ผ€์ผ ์‹œ ๋ฌธ์ œ).
  • ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ ์—†์Œ: ํ•™์Šต ์˜ˆ์ œ๋ผ ๋ฒ„ํผ/ํ”„๋กœ๊ทธ๋žจ ํ•ด์ œ๋Š” ์ƒ๋žต.

Code

๐Ÿ“ index.html

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

๐Ÿ“ style.css

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

๐Ÿ“ main.js

/**
 * ============================================================================
 * [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];
}

๐Ÿ“ vertex.glsl

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

๐Ÿ“ fragment.glsl

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

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

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