E D R , A S I H C RSS

WebGL


1. κΈ°λ³Έ 컨셉

OpenGLμ—μ„œ 정말 μ‹€λ¬΄μ—μ„œ μ“°λŠ” λΆ€λΆ„λ§Œ λ”°λ‘œ λ–Όμ–΄λ‚Έ OpenGL ES(Embeded System)의 Javascript κ΅¬ν˜„μ²΄μ΄λ©° HTML5 CanvasλΌ ν†΅ν•΄ λ‚˜νƒ€λ‚œλ‹€. λ”°λΌμ„œ μ΄ˆλ³΄μžκ°€ μ‰½κ²Œ λ°°μš°λŠ”λ°μ— 초점이 λ§žμΆ”μ–΄μ Έ μžˆμ§€ μ•Šκ³  였직 μ „λ¬Έκ°€κ°€ κ΅¬ν˜„μ„ ν•˜λŠ”λ°μ— 초점이 λ§žμΆ”μ–΄μ Έ μžˆλ‹€.

2. νŠΉμ§•

Javascriptμž„μ—λ„ λΆˆκ΅¬ν•˜κ³  마치 Cν”„λ‘œκ·Έλž˜λ° μŠ€νƒ€μΌμ˜ ν•¨μˆ˜λ“€μ΄ μ‘΄μž¬ν•œλ‹€. WinAPIκ°€ CμŠ€νƒ€μΌμ˜ OOPμ΄λ“ WebGL λ˜ν•œ CμŠ€νƒ€μΌμ˜ OOP이닀. λͺ¨λ“  ν•¨μˆ˜λŠ” WebGLcontextλΌλŠ” 객체에 μžˆλŠ”λ° 보면 κ·Έλƒ₯ μ ‘λ‘μ–΄λΌ λΆ™μ΄λŠ” λŠλ‚Œμ΄λ‹€.
var gl = canvas.getContext("experimental-webgl");
gl.attachShader(shaderProgram, fragmentShader);  
gl.attachShader(shaderProgram, vertexShader);
μœ„μ˜ μ½”λ“œλΌ λ³΄λ©΄ 쉐이더 ν”„λ‘œκ·Έλž¨μ— fragmentShader와 vertexShaderλΌ Link μ‹œν‚€λŠ” ꡬ문인데 주체인 shaderProgram은 첫번쨰 인자이고 gl은 κ·Έλƒ₯ 접두어 처럼 보인닀. μ € ꡬ문만 κ·ΈλŸ°κ²ƒμ΄ μ•„λ‹ˆλΌ λ‹€λ₯Έ λͺ¨λ“  ν•¨μˆ˜λ“€μ΄ μ € gl 객체에 λΆ™μ–΄μžˆλ‹€. ν•˜μ§€λ§Œ μ •μž‘ gl이 주체가 μ•„λ‹Œ 것듀이 λ§Žλ‹€. λ”°λΌμ„œ λž˜ν•‘ν•œ κ°μ²΄λΌ λ§Œλ“€μ–΄ μ“°λŠ” 것이 μ†νŽΈν•œλ° μ–΄μ„€ν”„κ²Œ ν–ˆλ‹€κ°€λŠ” 무척 꼬이게 λœλ‹€.

이 κ΄€μŠ΅μ€ OpenGL이 기본적으둜 C라이브러리이라 κ·ΈλŸ°λ“ ν•˜λ‹€. μ‹€μ œ λž˜ν•‘μ„ 진행해본결과 마치 MFCλΌ λ³΄λŠ”λ“ν•œ λŠλ‚Œμ„ κ°•ν•˜κ²Œ λ°›κ³  μžˆλ‹€.

2.1. OpenGL과 차이점

  • WebGL은 κΈ°μ‘΄ OpenGLκ³Ό λ‹€λ₯΄κ²Œ 직접 그리기가 μ§€μ›λ˜μ§€ μ•ŠλŠ”λ‹€. 기쑴의 glBegin()와 glEnd()μ‚¬μ΄μ—μ„œ 값을 κ³„μ†μ μœΌλ‘œ μ „λ‹¬ν•˜μˆ˜ μ—†κ³  였직 glDrawElement()λΌ ν†΅ν•œ 배열을 ν•œκΊΌλ²ˆμ— μ „λ‹¬ν•˜λŠ” 것'만' μ§€μ›ν•œλ‹€. μ΄ˆλ³΄μžλ“€μ˜ μ²«λ‚œκ΄€μ΄λ‹€.
  • μ‚¬κ°ν˜•κ·Έλ¦¬κΈ° 및 λ‹€κ°ν˜• 그리기가 μ§€μ›λ˜μ§€ μ•ŠλŠ”λ‹€. μ‹€μ œλ‘œ λ‹€κ°ν˜• κ·Έλ¦¬κΈ°λŠ” μ—°μŠ΅μ‹œμ—λ§Œ 자주 μ“°κ³  μ‹€μ œ μ½”λ“œμ—μ„œλŠ” μ‚Όκ°ν˜•μœΌλ‘œ 이루어진 λͺ¨λΈμ„ κ°€μ Έλ‹€ μ“°κΈ° λ•Œλ¬ΈμΈ κ²ƒμœΌλ‘œ 보인닀. 그리고 λ‹€κ°ν˜•μ€ μ‚Όκ°ν˜•μ˜ μ§‘ν•©μœΌλ‘œ ν‘œν˜„ν• μˆ˜ μžˆλ‹€.
  • μœ ν‹ΈλΌμ΄λΈŒλŸ¬λ¦¬λ‘œ μ œκ³΅λ˜λŠ” 큐브, ꡬ, 싀린더, ν‹°ν¬νŠΈκ°€ λͺ¨λ‘ μ§€μ›λ˜μ§€ μ•ŠλŠ”λ‹€. μ—­μ‹œ μ˜ˆμ œμ—λ§Œ 쓰이고 쓰지 μ•ŠκΈ° λ•Œλ¬Έμ— 과감히 μ œκ±°ν•œκ²ƒμœΌλ‘œ 보인닀.
  • 광원, 카메라 μ‘°μž‘, νšŒμ „ 등이 μ œκ³΅λ˜μ§€ μ•ŠλŠ”λ‹€. λͺ¨λ‘ μžμ‹ μ΄ 직접 연산을 톡해 행렬을 ꡬ해주어야 ν•œλ‹€. μ΄ˆλ³΄μžλ“€μ˜ λ‘˜μ¨° λ‚œκ΄€μ΄λ‹€.
  • ν…μŠ€μ³ λͺ¨λ“œμ™€ μ‘°λͺ… λͺ¨λ“œκ°€ 맀우 μ œν•œλ˜μ–΄ μžˆλ‹€.
  • μ‰μ΄λ”λΌ μ§œμ§€μ•ŠμœΌλ©΄ μ“Έμˆ˜κ°€ μ—†λ‹€. 심지어 ν…μŠ€μ³λΌ μž…νžˆλŠ” 것도 μ‰μ΄λ”μ—μ„œ μ²˜λ¦¬ν•œλ‹€. κ·Έλƒ₯ λ‹¨μƒ‰μœΌλ‘œ μ²˜λ¦¬ν•˜λŠ” μ½”λ“œλ„ 쉐이더 μ½”λ“œλΌ μ§œμ§€ μ•ŠμœΌλ©΄ κ·Έλƒ₯ 햐얀 κ²ƒλ§Œ λ³΄κ²Œλœλ‹€. 그리고 그것도 νšŒμ „μ‹œν‚¬μˆ˜λ„ μ—†λ‹€.

3. μ£Όμš”μš”μ†Œ

3.1. νŒŒμ΄ν”„ 라인

WebGL은 μΌμ •ν•œ νλ¦„κ΅¬μ‘°λΌ λ§Œλ“€μ–΄ 두고 κ·Έ 각뢀뢄을 λ§Œλ“€μˆ˜ μžˆλ„λ‘ ν•΄ λ‘μ—ˆλ‹€. μ•„λ§ˆ μ΅œμ ν™”κ°€ μ‰¬μš΄ 탓에 κ·ΈλŸ¬ν–ˆμœΌλ¦¬κ³  μƒκ°λœλ‹€.
WebGlνŒŒμ΄ν”„λΌμΈ
[PNG image (52.47 KB)]

AttributeλŠ” 각 포인트 λ³„λ‘œ μ „λ‹¬λ˜λŠ” 정보이고 uniform 은 μ „μ²΄μ—μ„œ 곡톡적인 정보이닀. 일반적으둜 AttributeλŠ” 각 μ •μ μ˜ μœ„μΉ˜ 정보와 각 μ§€μ μ˜ 법선 벑터 정보λΌμ„ μ „λ‹¬ν•œλ‹€. uniform은 일반적으둜 μΉ΄λ©”λΌμ˜ μœ„μΉ˜λ‚˜ ν™˜κ²½κ΄‘μ˜ μœ„μΉ˜μ²˜λŸΌ 전체적인 것을 μ „λ‹¬ν•œλ‹€. Attributeλ‚˜ uniform은 μΌμ’…μ˜ λ³€μˆ˜μΈλ° 핸듀을 μ–»μ–΄μ™€μ„œ 그것을 톡해 값을 μ „λ‹¬ν• μˆ˜ μžˆλ‹€. 즉 Atrributeλ‚˜ Uniform은 JavascriptμΈ‘μ—μ„œ μ‰μ΄λ”λ‘œ μ •λ³΄λΌ λ³΄λ‚΄λŠ” 것이닀. varying은 쉐이더 κ°„μ˜ 정보 전달에 μ‚¬μš©λœλ‹€. vertex shaderμ—μ„œ fragment shader둜 값이 μ „λ‹¬λ˜λ©° λ°˜λŒ€λŠ” λΆˆκ°€λŠ₯ν•˜λ‹€(νŒŒμ΄ν”„λΌμΈ ꡬ쑰상 λ‹Ήμ—°ν•œ 것이닀). μ΄λ•Œ vertex shaderλŠ” 각 정점(꼭지점) fragment shaderλŠ” 각 픽셀에 ν•œλ²ˆ 호좜되게 λ˜λŠ”λ° 각 정점 μ‚¬μ΄μ˜ 값듀은 보간법을 거쳐 μ „λ‹¬λ˜κ²Œ λœλ‹€(κ·ΈλΌλ””μ–ΈνŠΈ 같은 λŠλ‚Œμ΄λ‹€ 쀑간값을 μ•Œμ•„μ„œ λ§Œλ“€μ–΄ μ€λ‹€).

각 μ‰μ΄λ”λŠ” λ™μ‹œμ— λ™μž‘ν• μˆ˜ μžˆλŠ”λ° λ‹Ήμ—°νžˆ 이듀은 μ„œλ‘œκ°„μ— 독립적이어야 ν•œλ‹€.

3.2. 쉐이더

μ‰μ΄λ”λŠ” 쉐이더 μ–Έμ–΄λ‘œ λ”°λ‘œ 짜주고 컴파일 ν•΄μ•Όν•˜λ©° 심지어 λ§ν¬κΉŒμ§€ μ‹œμΌœμ£Όμ–΄μ•Ό ν•œλ‹€. GPU의 κ°•λ ₯ν•œ ν–‰λ ¬μ—°μ‚° λŠ₯λ ₯을 κ°€μ Έλ‹€ μ“°κΈ° μœ„ν•΄μ„œμΈκ²ƒμœΌλ‘œ λ³΄μ΄λŠ”λ° 이것을 μ‚¬μš©ν•˜μ§€ μ•Šκ³ μ„œλŠ” μ˜ˆμ œνŒŒμΌλ„ λŒλ €λ³Όμˆ˜κ°€ μ—†λ‹€. 닀행이 μ–Έμ–΄λŠ” C언어와 맀우 μœ μ‚¬ν•˜κ³  행렬연산이 λͺ¨λ‘ 있기 λ•Œλ¬Έμ— λ”±νžˆ μ–΄λ ΅κ±°λ‚˜ ν•˜μ§„ μ•Šλ‹€. λ‹€λ§Œ μ–΄λŠλΆ€λΆ„μ—μ„œ 어디와 μ—°κ²°λ˜λŠ”μ§€ μ΄ν•΄ν•˜λŠ”λ° μ‹œκ°„μ΄ κ±Έλ¦°λ‹€.

3.2.1. vertex shader

각 정점(vertex, 꼭지점)λ§ˆλ‹€ 호좜되며 주둜 κΌ­μ§€μ μ˜ μœ„μΉ˜λΌ μ—°μ‚°ν•˜κ³  μ‹€μ œ View에 νˆ¬μ˜ν•˜λŠ” 연산을 주둜 ν•˜κ²Œ λœλ‹€. ν•œλ§ˆλ””λ‘œ λͺ¨λΈμ˜ μœ„μΉ˜ λ³€ν™˜κ³Ό 카메라 μ‹œμ μ— λ”°λ₯Έ λ³€ν™˜ 원근법을 μ μš©ν•˜λŠ” λ³€ν™˜λ“±μ„ μˆ˜ν–‰ν•œλ‹€.

3.2.2. fragment shader

각 정점 사이에 μžˆλŠ” ν”½μ…€ λ§ˆλ‹€ ν˜ΈμΆœλœλ‹€. 주둜 κ΄‘μ›νš¨κ³ΌλΌ μ μš©ν•œ ν”½μ…€μ˜ μ΅œμ’…μ μΈ μƒ‰κΉ”μ΄λ‚˜ ν…μŠ€μ³ 연산에 μ‚¬μš©λœλ‹€. varyingλ³€μˆ˜λΌ vertex shaderμ—μ„œ fragment shader둜 λ„˜κ²¨μ£Όλ©΄ 각 정점 μ‚¬μ΄μ—λŠ” λ³΄κ°„λ²•μœΌλ‘œ λ³€ν™˜λœ 값이 λ„˜μ–΄ μ˜¨λ‹€.

4. μ˜ˆμ œμ½”λ“œ

μ•„λž˜ μ½”λ“œλŠ” μ •ν™•ν•˜μ§€ μ•ŠμœΌλ©° μ—°μŠ΅ λ„μ€‘μ˜ μ½”λ“œμž…λ‹ˆλ‹€. λ˜ν•œ WebGL의 νŠΉμ„±μƒ μ½”λ“œκ°€ λΆ„μ‚°λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ 객체 λž˜ν•‘μ„ μ§„ν–‰μ€‘μž…λ‹ˆλ‹€.

ν˜„μž¬ 객체 λž˜ν•‘μ€‘ μ€‘λŒ€ν•œ λ¬Έμ œμ— 봉착. λŒ€λΆ€λΆ„μ˜ λͺ¨λ“ˆκ³Ό 세이더 μ½”λ“œλŠ” 콜백으둜 ν˜ΈμΆœλ˜λŠ”λ° 이것을 적절히 λž˜ν•‘ν•  방법이 μ—†λ‹€. webGLκ³ΌλŠ” ν•˜λ“± 연관이 μ—†λŠ” λΆ€λΆ„μ΄λΌμ„œ 각자 μ•Œμ•„μ„œ κ΅¬ν˜„ν•˜λ„λ‘ 해도 λ˜μ§€λ§Œ λŒ€λΆ€λΆ„μ˜ 경우 같은 μ½”λ“œλΌ λ‹€μŠ€ 짜고 μžˆλŠ” λ‚˜λΌ λ³΄κ²Œ λœλ‹€. 이것을 μ–΄λ–»κ²Œ ν•΄μ•Ό μž˜ν•œ λž˜ν•‘μ΄λΌ ν• μˆ˜ μžˆμ„κΉŒ?

4.1. vertexShader

attribute vec3 aVertexPosition;
attribute vec3 aVertexNormal;

uniform mat4 matCamara;
uniform mat4 matProject;

uniform vec3 lightPos;
uniform vec3 lightDirection;
uniform vec4 materialDiffuse;
uniform vec4 lightDiffuse;

varying vec4 vFinalColor;
varying vec3 vNormal;

void main(void){

	vec3 N = normalize((vec4(aVertexNormal, 1.0) * matCamara).xyz);//nomal compute
	vec3 L = normalize(lightDirection); //lightDrection

	float lambertTerm = max(dot(N, -L), 0.0);
	vec4 Id =  lightDiffuse * materialDiffuse * lambertTerm;

	vNormal = normalize((vec4(aVertexNormal, 1.0) * matCamara).xyz);

	vFinalColor = Id;
	vFinalColor.a = 1.0;
	gl_Position = matProject * matCamara * vec4((aVertexPosition), 1.0);
	gl_Position.w = 1.0;
}

4.2. fragmentShader

#ifdef GL_ES
precision highp float;
#endif

uniform vec3 lightPos;
uniform vec3 lightDirection;
uniform vec4 materialDiffuse;
uniform vec4 lightDiffuse;
	
varying vec4 vFinalColor;
varying vec3 vNormal;

void main(void) {

	vec3 L = normalize(lightDirection);

    gl_FragColor = vFinalColor + vec4(0.01,0.01,0.01, 1.0);
}

4.3. javascript

var cube = {
  "vertices": [
     0.2, 0.2, 0.2, //0
     0.2, 0.2,-0.2, //1
     0.2,-0.2, 0.2, //2
     0.2,-0.2,-0.2, //3
    -0.2, 0.2,-0.2, //4
    -0.2, 0.2, 0.2, //2
    -0.2,-0.2,-0.2, //6
    -0.2,-0.2, 0.2  //7
  ],
  "normals": [
     1, 1, 1, //0
     1, 1,-1, //1
     1,-1, 1, //2
     1,-1,-1, //3
    -1, 1,-1, //4
    -1, 1, 1, //5
    -1,-1,-1, //6
    -1,-1, 1  //7

  ],
  "indices" : [
    0,2,3, 0,3,1,
    4,6,7, 4,7,5,
    4,5,0, 4,0,1,
    7,6,3, 7,3,2,
    5,7,2, 5,2,0,
    1,3,6, 1,6,4
  ]
}

var init = function(){
  var gl = getGLContext();


  var cubeBuffer = new GLBuffer(gl, cube);

  var shader;
  async.parallel([
    function(callback){
      var url = document.getElementById("vertexShader").getAttribute("src");
      ajax(url, callback);
    },
    function(callback){
      var url = document.getElementById("fragmentShader").getAttribute("src");
      ajax(url, callback);
    },
  ], function (err, data){
    shader = new GLShader(gl, data[0], data[1]);

    gl.useProgram(shader.program);
    shader.aVertexPosition = gl.getAttribLocation(shader.program, "aVertexPosition");
    shader.aVertexNormal = gl.getAttribLocation(shader.program, "aVertexNormal");

    var cam = gl.getUniformLocation(shader.program, "matCamara");
    
    var camMat = mat4.identity(mat4.create());

    mat4.translate(camMat, camMat, [0, 0, 0.1]);
    mat4.rotate(camMat, camMat, Math.PI/4, [1,0.5,0.5]);

    gl.uniformMatrix4fv(cam, false, camMat);

    var lightPos = shader.getUniformLocation("lightPos");
    gl.uniform3fv(lightPos, [0.1,0.1,0.1]);

    var lightDirection = shader.getUniformLocation("lightDirection");
    gl.uniform3fv(lightDirection, [-1, -1, -1]);
    
    var materialDiffuse = shader.getUniformLocation("materialDiffuse");
    gl.uniform4fv(materialDiffuse, [0.8, 0.2, 0.2, 1.0]);

    var lightDiffuse = shader.getUniformLocation("lightDiffuse");
    gl.uniform4fv(lightDiffuse, [1,1,1,1]);

    var matProject = mat4.identity(mat4.create());//2PI = 360d -> 1d = PI/180
    mat4.perspective(matProject, Math.PI/180 * 80, 1, 0, 1);

    gl.uniformMatrix4fv(
      shader.getUniformLocation("matProject"),
      false,
      matProject
    );

    onReady(gl, cubeBuffer, shader);
  });
}

setTimeout(init, 0);
function onReady(gl, buffer, shader){
  onDraw();

  function onDraw(){
    gl.clearColor(0.0, 0.0, 0.0, 1.0);

    gl.enable(gl.DEPTH_TEST);

    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.viewport(0,0,300,300);

    gl.enableVertexAttribArray(shader.aVertexPosition);
    gl.enableVertexAttribArray(shader.aVertexNormal);

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer.vertex);
    gl.vertexAttribPointer(shader.aVertexPosition, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer.normal);
    gl.vertexAttribPointer(shader.aVertexNormal, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.index);
    gl.drawElements(gl.TRIANGLES, buffer.index.length, gl.UNSIGNED_SHORT, 0);
  }
}

function ajax(url, callback){
  var ajax = new XMLHttpRequest();
  ajax.onreadystatechange = function(){
    if(ajax.readyState === 4){
      //complete requset
      if(ajax.status === 200){
        //not error
        callback(null, ajax.responseText);
      }
    }
  }

  ajax.open("GET", url, true);//if need Sync method set false;
  ajax.send(null);
}
//Lib function
function getGLContext(){
  var canvas = document.getElementsByTagName("canvas");
    
  canvas = [].filter.call(canvas, function(element){
    if(element.getAttribute("WebGL") != null)
      return true;
    else
      return false;
  });
  canvas = canvas[0];

  return canvas.getContext("experimental-webgl");
}


//Lib Class
function GLBuffer(gl, model){
  this.model = model;

  try {
    //only binded buffer can send data
    //vertex is coord of points
    var vertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(model.vertices), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    //index is triangle point index of suface
    var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(model.indices), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    //normals Buffer
    var normalBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(model.normals), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    this.vertex = vertexBuffer;
    this.index = indexBuffer;
    this.index.length = model.indices.length;
    this.normal = normalBuffer;
  } catch(e){
    throw Error("Can not create Buffer");
  }
}
function GLShader(gl, vertexSource, fragmentSource){
  var shaderProgram = gl.createProgram();

  //compile Source
  var vertexShader = gl.createShader(gl.VERTEX_SHADER);
  gl.shaderSource(vertexShader, vertexSource);
  gl.compileShader(vertexShader);

  checkCompile(vertexShader);

  var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  gl.shaderSource(fragmentShader, fragmentSource);
  gl.compileShader(fragmentShader);

  checkCompile(fragmentShader);

  //attach shader
  gl.attachShader(shaderProgram, fragmentShader);
  
  gl.attachShader(shaderProgram, vertexShader);
  
  //link
  gl.linkProgram(shaderProgram);

  this.program = shaderProgram;

  this._private = {
    gl : gl
  }

  function checkCompile(shader){
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      throw Error(gl.getShaderInfoLog(shader));
    }
  }
}
GLShader.prototype.getUniformLocation = function(name){
  return this._private.gl.getUniformLocation(this.program, name);
}
GLShader.prototype.uniform4fv = function(name, arr){
  this._private.gl.uniform4fv(this.getUniformLocation(name), arr);
}
GLShader.prototype.uniform3fv = function(name, arr){
  this._private.gl.uniform3fv(this.getUniformLocation(name), arr);
}
GLShader.prototype.uniformMatrix4fv = function(name, arr){
  this._private.gl.uniformMatrix4fv(this.getUniformLocation(name), false, arr);
}

5. WebGL의 μ’Œν‘œκ³„

OpenGLκ³Ό λ™μΌν•œ -1.0 ~ 1.0이며 μ΄λΌ λ„˜μ–΄κ°ˆμ‹œμ—λŠ” ν‘œν˜„λ˜μ§€ μ•ŠλŠ”λ‹€. 맀트릭슀 연산을 μ§μ ‘ν•΄μ•Όλ§Œ ν•˜λŠ” WebGLμ—μ„œλŠ” 이점이 κ°„κ³Όλ˜κΈ° μ‰¬μ›Œμ„œ μ΄λΈ κ·Έλ¦° νŽ˜μ΄μ§€κ°€ 어디에 μžˆλŠ”μ§€ μ°ΎλŠ” ν˜„μƒμ΄ λ°œμƒν•˜κ²Œ λœλ‹€.

계속 μž‘μ„±μ€‘.
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:28:23
Processing time 0.0756 sec