목차

WebGLFundamentals.org

Fix, Fork, Contribute

WebGL 텍스트 - HTML

이 포스트는 WebGL 관련 시리즈에서 이어집니다. 아직 읽지 않았다면 거기부터 시작하는 게 좋습니다.

"WebGL에서 텍스트를 그리는 방법"에 대해 질문을 많이 받는데요. 먼저 스스로에게 물어볼 것은 텍스트를 그리는 목적입니다. 여러분은 브라우저에 있고, 브라우저는 텍스트를 표시합니다. 따라서 첫 번째 답은 HTML을 사용하여 텍스트를 표시하는 겁니다.

가장 쉬운 예시를 먼저 들어보면, WebGL 위에 텍스트를 그리고 싶다고 해봅시다. 이걸 텍스트 오버레이라고 부르는데요. 기본적으로 이것은 같은 위치에 있는 텍스트입니다.

가장 간단한 방법은 HTML 요소를 만들고 CSS를 사용하여 겹치도록 만드는 겁니다.

예시: 먼저 컨테이너를 만들고, 컨테이너 안에 겹치게 할 캔버스와 HTML을 넣습니다.

<div class="container">
  <canvas id="canvas"></canvas>
  <div id="overlay">
    <div>Time: <span id="time"></span></div>
    <div>Angle: <span id="angle"></span></div>
  </div>
</div>

다음으로 캔버스와 HTML을 겹치게 하기 위한 CSS를 설정합니다.

.container {
    position: relative;
}
#overlay {
    position: absolute;
    left: 10px;
    top: 10px;
}

이제 초기화할 때 해당 요소들을 탐색해, 변경하려는 영역을 만들거나 찾습니다.

// 영향을 주고 싶은 요소 탐색
var timeElement = document.querySelector("#time");
var angleElement = document.querySelector("#angle");

// 브라우저의 시간을 아끼기 위해 텍스트 노드 생성
var timeNode = document.createTextNode("");
var angleNode = document.createTextNode("");

// 놓아야 할 곳에 해당 텍스트 노드 추가
timeElement.appendChild(timeNode);
angleElement.appendChild(angleNode);

마지막으로 렌더링할 때 노드를 업데이트합니다.

function drawScene() {
    ...

    // 회전 라디안에서 도 단위로 전환
    var angle = radToDeg(rotation[1]);

    // 0 - 360
    angle = angle % 360;

    // 노드 설정
    angleNode.nodeValue = angle.toFixed(0);  // 소수점 없음
    timeNode.nodeValue = clock.toFixed(2);   // 소수점 이하 2자리

그리고 여기 해당 예제입니다.

변경하려는 부분에 대해 div 내부에 span을 어떻게 넣었는지 확인하세요. 여기에서는 이 방법이 span 없이 div를 사용하는 것보다 빠르다고 가정하고 있습니다.

timeNode.value = "Time " + clock.toFixed(2);

또한 node = document.createTextNode()와 이후에 node.nodeValue = someMsg를 호출하여 텍스트 노드를 사용하고 있습니다. someElement.innerHTML = someHTML도 사용할 수 있는데요. 임의의 HTML 문자열을 삽입할 수 있기 때문에 더 유연하지만, 설정할 때마다 브라우저가 노드를 생성하고 파괴해야 하기 때문에 약간 느릴 수 있습니다. 뭐가 더 좋을지는 여러분이 선택하시면 됩니다.

오버레이 기법의 중요한 점은 WebGL이 브라우저에서 실행된다는 겁니다. 적절하게 브라우저의 기능을 사용하는 걸 잊지마세요. 많은 OpenGL 프로그래머들은 앱의 모든 부분을 처음부터 100% 렌더링하는 것에 익숙하지만, WebGL은 브라우저에서 실행되기 때문에 이미 많은 기능들을 가지고 있습니다. 그 기능들을 활용하세요. 여러 장점이 있습니다. 예를 들면 CSS를 사용하여 해당 오버레이에 재미있는 스타일을 쉽게 부여할 수 있죠.

여기 동일한 예제에 일부 스타일을 추가했습니다. 모서리가 둥글고, 글자 주변이 빛납니다. 그리고 테두리는 빨간색이죠. HTML을 사용하면 이 모든 기능을 자유롭게 사용할 수 있습니다.

다음으로 가장 일반적으로 원하는 것은 렌더링하는 대상을 기준으로 일부 텍스트를 배치하는 겁니다. 이것도 HTML로 할 수 있습니다.

이 경우 캔버스가 있는 컨테이너와 HTML을 움직이기 위한 또 다른 컨테이너를 만듭니다.

<div class="container">
  <canvas id="canvas" width="400" height="300"></canvas>
  <div id="divcontainer"></div>
</div>

그리고 CSS를 설정할 겁니다.

.container {
    position: relative;
    overflow: none;
    width: 400px;
    height: 300px;
}

#divcontainer {
    position: absolute;
    left: 0px;
    top: 0px;
    width: 100%;
    height: 100%;
    z-index: 10;
    overflow: hidden;
}

.floating-div {
    position: absolute;
}

position: absolute; 부분은 #divcontainerposition: relativeposition: absolute인 첫 번째 조상을 기준으로 절대적인 위치를 가지도록 합니다. 이 경우에는 캔버스와 #divcontainer가 있는 컨테이너가 됩니다.

left: 0px; top: 0px#divcontainer가 정렬되도록 만듭니다. z-index: 10는 컨테이너가 캔버스 위에 떠있도록 만들죠. 그리고 overflow: hidden은 부모의 영역을 벗어난 자식을 안 보이도록 만듭니다.

마지막으로 .floating-div는 위치 지정이 가능한 div 생성에 사용될 겁니다.

이제 div 컨테이너를 찾고, div를 만들어 추가해야 합니다.

// div 컨테이너 탐색
var divContainerElement = document.querySelector("#divcontainer");

// div 생성
var div = document.createElement("div");

// CSS 클래스 할당
div.className = "floating-div";

// 내용에 대한 텍스트 노드 생성
var textNode = document.createTextNode("");
div.appendChild(textNode);

// divcontainer에 추가
divContainerElement.appendChild(div);

이제 스타일을 설정하여 div의 위치를 조정할 수 있습니다.

div.style.left = Math.floor(x) + "px";
div.style.top  = Math.floor(y) + "px";
textNode.nodeValue = clock.toFixed(2);

다음은 div를 기준으로 튕기는 예제입니다.

다음 단계는 3D 장면의 무언가를 기준으로 배치하는 겁니다. 어떻게 할까요? Perspective projection을 다룰 때 GPU에 요청한 방법과 정확히 동일하게 수행합니다.

위 예제를 통해 행렬을 사용하는 방법, 곱하는 방법, 클립 공간으로 전환하기 위해 투영 행렬를 적용하는 방법을 배웠습니다. 이 모든 것을 셰이더로 전달하고 지역 공간의 정점에 곱하여 클립 공간으로 전환합니다. 자바스크립트에서도 이 모든 계산을 수행할 수 있는데요. 클립 공간(-1 ~ +1)을 픽셀로 곱하고 이를 사용하여 div를 배치할 수 있습니다.

gl.drawArrays(...);

// F를 3D로 그리기 위해 행렬 계산

// 'F'의 지역 공간에서 한 점 선택
// X  Y  Z  W
var point = [100, 0, 0, 1];  // 앞면 오른쪽 상단 모서리

// F에 대해 계산한 행렬을 사용하여 클립 공간 위치를 계산
var clipspace = m4.transformVector(matrix, point);

// GPU처럼 X와 Y를 W로 나누기
clipspace[0] /= clipspace[3];
clipspace[1] /= clipspace[3];

// 클립 공간을 픽셀로 변환
var pixelX = (clipspace[0] *  0.5 + 0.5) * gl.canvas.width;
var pixelY = (clipspace[1] * -0.5 + 0.5) * gl.canvas.height;

// div 배치
div.style.left = Math.floor(pixelX) + "px";
div.style.top  = Math.floor(pixelY) + "px";
textNode.nodeValue = clock.toFixed(2);

그리고 짜잔, div의 왼쪽 상단 모서리가 F의 오른쪽 상단 모서리 앞면에 완벽하게 정렬되었습니다.

물론 더 많은 텍스트를 원한다면 div를 더 만들 수 있습니다.

자세한 내용을 보시려면 마지막 예제의 소스 코드를 봐주세요. 한 가지 중요한 점은 DOM에서 HTML 요소를 생성하고, 추가하고, 제거하는 작업이 느리기 때문에, 위 예제에서는 생성하고 해당 요소들을 유지한다는 겁니다. 사용하지 않는 것은 DOM에서 지우지 않고 숨깁니다. 더 빠른지 알아보려면 정보를 수집해야 하는데요. 그게 제가 선택한 방법입니다.

텍스트에 HTML을 사용하는 방법이 잘 이해되셨길 바랍니다. 다음은 텍스트에 Canvas 2D를 사용하는 방법에 대해 다루겠습니다.

이슈/버그는? Github에 이슈를 만들어주세요.
코드 블록은 <pre><code>여기에 코드 입력</code></pre>를 사용해주세요
comments powered by Disqus