본문 바로가기

WebGL

WebGL Fundamentals > Shadows

https://webglfundamentals.org/webgl/lessons/webgl-shadows.html

 

WebGL Shadows

How to compute shadows

webglfundamentals.org

https://github.com/Myoungmin/WebGL_Fundamentals

 

GitHub - Myoungmin/WebGL_Fundamentals: WebGL 학습 프로젝트

WebGL 학습 프로젝트. Contribute to Myoungmin/WebGL_Fundamentals development by creating an account on GitHub.

github.com

 

 

WebGL Shadows

그림자를 그리는 데는 여러가지 방법이 있다.

가장 흔히 사용되는 방법은 그림자 맵을 사용해서 그림자를 그리는 것이다.

 

투영 맵핑에서 이미지는 장면에 있는 물체에 직접 그린 것이 아니라,

물체가 렌더링 될 때 각 픽셀에 대해 투영된 텍스처 범위 내에 있는지를 확인하고, 

범위 내에 있다면 투영된 텍스처로부터 적절한 색상을 샘플링하는 방식으로 그린다.

 

범위 밖이라면 물체에 맵핑된 다른 텍스처로부터 텍스처 좌표를 기반으로 색상을 샘플링한다.

 

만일 투영된 텍스처가 조명의 View에서 얻어진 깊이 데이터라면 어떻게 될까?

다시 말해 조명이 절두체의 끝부분에 존재하는 것처럼 가정하고, 

투영된 텍스처가 그 조명 위치에서의 깊이값을 가지고 있는 것이다.

 

그러한 데이터를 확보할 수 있다면 렌더링할 색상을 결정할 때,

투영된 텍스처로부터 깊이값을 얻어올 수 있고, 

그리려는 픽셀이 그 깊이값보다, 조명과 더 먼지 더 가까운지 알 수 있다.

 

만일 더 멀다면 무언가가 조명을 가리고 있어서 픽셀이 그림자 영역에 있다는 뜻이다.

 

 

 

위 그림에서는 깊이 텍스처가 절두체 조명 공간을 통해 투영되고 있다.

 

바닥면 픽셀을 그릴 때는 그 픽셀의 조명으로부터의 깊이를 계산한다.

위 그림 예시에서는 0.3으로 계산된다.

 

중간에 구가 존재하여 조명의 View에서 텍스처에 저장된 깊이값은 0.1이다.

 

0.1 < 0.3이므로 그 바닥면 픽셀은 그림자 범위에 있다는 것을 알 수 있다.

섀도우 맵 그리기

텍스처에 렌더링할 때 사용했던 depth renderbuffer는 텍스처로 사용할 수 없다.

 

대신에 WEBGL_depth_texture를 활성화 하면 깊이 텍스처를 제공할 수 있다.

 

깊이 텍스처를 사용하여 프레임 버퍼에 첨부한 다음,

나중에 셰이더에 대한 입력으로 텍스처를 사용한다.

WEBGL_depth_texture 확장이 있는지 확인하고 활성화하는 코드

function main() {
  // WebGL Context 얻기
  /** @type {HTMLCanvasElement} */
  const canvas = document.querySelector('#canvas');
  const gl = canvas.getContext('webgl');
  if (!gl) {
    return;
  }
 
  const ext = gl.getExtension('WEBGL_depth_texture');
  if (!ext) {
    return alert('need WEBGL_depth_texture');
  }

텍스처를 생성한 다음 프레임 버퍼를 생성하고, DEPTH_ATTACHMENT로 프레임 버퍼에 텍스처를 첨부

const depthTexture = gl.createTexture();
const depthTextureSize = 512;
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
gl.texImage2D(
    gl.TEXTURE_2D,      // 대상
    0,                  // 밉 레벨
    gl.DEPTH_COMPONENT, // 내부 포맷
    depthTextureSize,   // 너비
    depthTextureSize,   // 높이
    0,                  // 테두리
    gl.DEPTH_COMPONENT, // 포맷
    gl.UNSIGNED_INT,    // 타입
    null);              // 데이터
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
 
const depthFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer);
gl.framebufferTexture2D(
    gl.FRAMEBUFFER,       // 대상
    gl.DEPTH_ATTACHMENT,  // 어태치먼트 포인트
    gl.TEXTURE_2D,        // 텍스처 대상
    depthTexture,         // 텍스처
    0);                   // 밉 레벨

실제로 사용하지 않더라도 WebGL에서는 색상 텍스처를 생성하고 색상 어태치먼트로 첨부해야 한다.

OpenGL ES 2.0 스펙은 어태치먼 규칙에서 적어도 하나의 어태치먼트는 있어야 한다는 규칙때문이다.

 

// 깊이 텍스처와 같은 크기로 색상 텍스처 생성
const unusedTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, unusedTexture);
gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA,
    depthTextureSize,
    depthTextureSize,
    0,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    null,
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
 
// 프레임 버퍼에 첨부
gl.framebufferTexture2D(
    gl.FRAMEBUFFER,        // 대상
    gl.COLOR_ATTACHMENT0,  // 어태치먼트 포인트
    gl.TEXTURE_2D,         // 텍스처 대상
    unusedTexture,         // 텍스처
    0);                    // 밉 레벨

 

이를 사용하기 위해서는 서로 다른 셰이더를 사용해서 장면을 두 번 이상 그릴 수 있어야 한다.

한 번은 깊이 텍스처로 렌더링하기위한 간단한 셰이더를 사용해서 그린다.

다른 한 번은 텍스처를 투영하는 셰이더를 사용해서 그린다.

drawScene을 수정해서 렌더링을 수행하려는 프로그램을 전달

function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo) {
  // 카메라 행렬로 뷰 행렬을 만듭니다.
  const viewMatrix = m4.inverse(cameraMatrix);
 
  gl.useProgram(programInfo.program);
 
  // 구체와 평면에 모두 사용되는 유니폼을 설정합니다.
  // 주의: 셰이더에 대응되는 유니폼이 없는 경우 무시됩니다.
  webglUtils.setUniforms(programInfo, {
    u_view: viewMatrix,
    u_projection: projectionMatrix,
    u_textureMatrix: textureMatrix,
    u_projectedTexture: depthTexture,
  });
 
  // ------ 구체 그리기 --------
 
  // 속성 설정
  webglUtils.setBuffersAndAttributes(gl, programInfo, sphereBufferInfo);
 
  // 구에 필요한 유니폼 설정
  webglUtils.setUniforms(programInfo, sphereUniforms);
 
  // gl.drawArrays 또는 gl.drawElements 호출
  webglUtils.drawBufferInfo(gl, sphereBufferInfo);
 
  // ------ 평면 그리기 --------
 
  // 속성 설정
  webglUtils.setBuffersAndAttributes(gl, programInfo, planeBufferInfo);
 
  // 위에서 계산한 유니폼 설정
  webglUtils.setUniforms(programInfo, planeUniforms);
 
  // gl.drawArrays 또는 gl.drawElements 호출
  webglUtils.drawBufferInfo(gl, planeBufferInfo);
}

drawScene을 활용해 장면을 조명 View에서 그리고, 그 후에 깊이 텍스처를 사용해 그린다.

function render() {
  webglUtils.resizeCanvasToDisplaySize(gl.canvas);
 
  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);
 
  // 조명 시점에서 먼저 그립니다.
  const lightWorldMatrix = m4.lookAt(
      [settings.posX, settings.posY, settings.posZ],          // 위치
      [settings.targetX, settings.targetY, settings.targetZ], // 대상
      [0, 1, 0],                                              // 위쪽
  );
  const lightProjectionMatrix = settings.perspective
      ? m4.perspective(
          degToRad(settings.fieldOfView),
          settings.projWidth / settings.projHeight,
          0.5,  // 근거리
          10)   // 원거리
      : m4.orthographic(
          -settings.projWidth / 2,   // 왼쪽
           settings.projWidth / 2,   // 오른쪽
          -settings.projHeight / 2,  // 아래쪽
           settings.projHeight / 2,  // 위쪽
           0.5,                      // 근거리
           10);                      // 원거리
 
  // 깊이 텍스처에 그립니다.
  gl.bindFramebuffer(gl.FRAMEBUFFER, depthFramebuffer);
  gl.viewport(0, 0, depthTextureSize, depthTextureSize);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 
  drawScene(lightProjectionMatrix, lightWorldMatrix, m4.identity(), colorProgramInfo);
 
  // 이번에는 캔버스에 그리는데, 깊이 텍스처를 장면에 투영해서 그립니다.
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
 
  let textureMatrix = m4.identity();
  textureMatrix = m4.translate(textureMatrix, 0.5, 0.5, 0.5);
  textureMatrix = m4.scale(textureMatrix, 0.5, 0.5, 0.5);
  textureMatrix = m4.multiply(textureMatrix, lightProjectionMatrix);
  // 월드 행렬의 역행렬을 사용합니다.
  // 이렇게 하면 다른 위치 값들이 이 월드 공간에 상대적인 값이 됩니다.
  textureMatrix = m4.multiply(
      textureMatrix,
      m4.inverse(lightWorldMatrix));
 
  // 투영 행렬 계산
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const projectionMatrix =
      m4.perspective(fieldOfViewRadians, aspect, 1, 2000);
 
  // lookAt을 사용한 카메라 행렬 계산
  const cameraPosition = [settings.cameraX, settings.cameraY, 7];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(cameraPosition, target, up);
 
  drawScene(projectionMatrix, cameraMatrix, textureMatrix, textureProgramInfo); 
}

 

먼저 구와 평면을, 

절두체 라인을 그리기 위해 만든 프래그먼트 셰이더를 사용해, 

깊이 텍스처에 렌더링한다.

 

이 셰이더는 단색을 그리는 셰이더이고, 

특별히 다른 계산을 하고 있지 않은데, 

깊이 텍스처를 렌더링하는데는 이것이면 충분하다.

 

이후에 장면을 캔버스에 다시 그리는데,

전과 동일하게 텍스처를 장면에 투영해서 그린다.

셰이더에서 깊이 텍스처를 참조할 때 red 값만 유효하기 때문에, red, green, blue에 대해 같은 값을 반복하여 할당한다.

<!-- fragment shader -->
<script  id="fragment-shader-3d" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;
varying vec4 v_projectedTexcoord;

uniform vec4 u_colorMult;
uniform sampler2D u_texture;
uniform sampler2D u_projectedTexture;

void main() {
  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
  bool inRange =
      projectedTexcoord.x >= 0.0 &&
      projectedTexcoord.x <= 1.0 &&
      projectedTexcoord.y >= 0.0 &&
      projectedTexcoord.y <= 1.0;

  // 'r'채널에 깊이값이 저장되어 있습니다.
  vec4 projectedTexColor = vec4(texture2D(u_projectedTexture, projectedTexcoord.xy).rrr, 1);
  vec4 texColor = texture2D(u_texture, v_texcoord) * u_colorMult;
  float projectedAmount = inRange ? 1.0 : 0.0;
  gl_FragColor = mix(texColor, projectedTexColor, projectedAmount);
}
</script>

 

projectedDepth가 currentDepth보다 작으면

조명의 시점에서 더 가까운 물체가 있는 것이므로, 

그리려는 픽셀이 그림자 영역 안에 있는 것이다.

 

그림자가 없어야 하는 곳에 나타나는 이상한 패턴 : 섀도우 애크니(shadow acne)

깊이 텍스처에 저장된 깊이 데이터가 양자화되기 때문에 나타나는 현상이다.

 

이는 텍스처 자체가 픽셀의 그리드이기 때문이기도 하고,

조명의 시점으로 투영되어 생성되었으나, 

그 값을 카메라 시점에서 비교하고 있기 때문이기도 하다.  

 

다시말해 깊이 지도 격자의 값들이 카메라와 정렬되지 않아서,

currentDepth를 계산할 때 projectedDepth보다 약간 작거나 큰 경우가 생기기 때문이다.

 

바이어스를 더해서 해결할 수 있다.

바이어스를 더해주는 로직을 Fragment Shader에 적용

//...
 
uniform float u_bias;
 
void main() {
  vec3 projectedTexcoord = v_projectedTexcoord.xyz / v_projectedTexcoord.w;
  float currentDepth = projectedTexcoord.z + u_bias;
 
  bool inRange = 
      projectedTexcoord.x >= 0.0 &&
      projectedTexcoord.x <= 1.0 &&
      projectedTexcoord.y >= 0.0 &&
      projectedTexcoord.y <= 1.0;
 
  float projectedDepth = texture2D(u_projectedTexture, projectedTexcoord.xy).r;
  float shadowLight = (inRange && projectedDepth <= currentDepth) ? 0.0 : 1.0;  
 
  vec4 texColor = texture2D(u_texture, v_texcoord) * u_colorMult;
  gl_FragColor = vec4(texColor.rgb * shadowLight, texColor.a);
}

바이어스 값을 설정해준다.

const settings = {
  cameraX: 2.75,
  cameraY: 5,
  posX: 2.5,
  posY: 4.8,
  posZ: 4.3,
  targetX: 2.5,
  targetY: 0,
  targetZ: 3.5,
  projWidth: 1,
  projHeight: 1,
  perspective: true,
  fieldOfView: 120,
  bias: -0.006,
};
 
//...
 
function drawScene(projectionMatrix, cameraMatrix, textureMatrix, programInfo, /**/u_lightWorldMatrix) {
  // 카메라 행렬로 뷰 행렬을 만듭니다.
  const viewMatrix = m4.inverse(cameraMatrix);
 
  gl.useProgram(programInfo.program);
 
  // 구와 평면에 모두 사용되는 유니폼을 설정합니다.
  // 주의: 셰이더에 대응되는 유니폼이 없는 경우 무시됩니다.
  webglUtils.setUniforms(programInfo, {
    u_view: viewMatrix,
    u_projection: projectionMatrix,
    u_bias: settings.bias,
    u_textureMatrix: textureMatrix,
    u_projectedTexture: depthTexture,
  });
 
  //...

 

스포트라이트를 적용하여 그림자 표현

innerLimit와 outerLimit은 도트 공간(코사인 공간)의 값이고,

조명의 방향을 따라서 뻗어나가는 형식이기 때문에, 

시야각의 절반만 필요하다.

 

// 시야각의 절반
u_innerLimit: Math.cos(degToRad(settings.fieldOfView / 2 - 10)),
u_outerLimit: Math.cos(degToRad(settings.fieldOfView / 2)),

 

4x4 행렬의 세 번째 행이 Z축이므로, 

lightWorldMatrix로부터 세 번째 행의 앞 세개 값을 가져오면, 

그것이 조명의 -Z방향이라는 것을 알 수 있다.

우리는 양의 방향이 필요하기 때문에 이것을 뒤집는다.

 

// 세 번째 행의 앞 세개 값을 가져온다
// 양의 방향이 필요하기 때문에 뒤집는다.
u_lightDirection: lightWorldMatrix.slice(8, 11).map(v => -v),

 

네 번째 행이 월드공간 위치라는 것을 알고 있으므로, 

관련된 행렬로부터 lightWorldPosition과 viewWorldPosition(카메라의 월드 공간 위치)을 얻을 수 있다.

 

// 네 번째 행으로 lightWorldPosition과 viewWorldPosition(카메라의 월드 공간 위치)을 얻는다.
u_lightWorldPosition: lightWorldMatrix.slice(12, 15),
u_viewWorldPosition: cameraMatrix.slice(12, 15),

위 내용이 적용된 drawScene, render

function drawScene(
    projectionMatrix,
    cameraMatrix,
    textureMatrix,
    lightWorldMatrix,
    programInfo) {
  // 카메라 행렬로부터 뷰 행렬을 만듭니다.
  const viewMatrix = m4.inverse(cameraMatrix);
 
  gl.useProgram(programInfo.program);
 
  // 구와 평면에 모두 사용되는 유니폼을 설정합니다.
  // 주의: 셰이더에 대응되는 유니폼이 없는경우 무시됩니다.
  webglUtils.setUniforms(programInfo, {
    u_view: viewMatrix,
    u_projection: projectionMatrix,
    u_bias: settings.bias,
    u_textureMatrix: textureMatrix,
    u_projectedTexture: depthTexture,
    u_shininess: 150,
    u_innerLimit: Math.cos(degToRad(settings.fieldOfView / 2 - 10)),
    u_outerLimit: Math.cos(degToRad(settings.fieldOfView / 2)),
    u_lightDirection: lightWorldMatrix.slice(8, 11).map(v => -v),
    u_lightWorldPosition: lightWorldMatrix.slice(12, 15),
    u_viewWorldPosition: cameraMatrix.slice(12, 15),
  });
 
//...
 
function render() {
  //...
 
  drawScene(
      lightProjectionMatrix,
      lightWorldMatrix,
      m4.identity(),
      lightWorldMatrix,
      colorProgramInfo);
 
  //...
 
  drawScene(
      projectionMatrix,
      cameraMatrix,
      textureMatrix,
      lightWorldMatrix,
      textureProgramInfo);
 
  //...
}

 

 

 

 

 

 

https://myoungmin.github.io/WebGL_Fundamentals/

 

WebGL_Fundamentals

WebGL이란? WebGL은 Web Graphics Library의 약자로 웹상에서 2D 및 3D 그래픽을 렌더링하기 위한 로우 레벨 Javascript API. 학습내용 블로그 정리

myoungmin.github.io