가상 아트 세션

아트 세션 세부정보

요약

6명의 아티스트가 VR로 그림을 그리며 디자인하고 조각하는 프로그램에 초대받았습니다. 이 과정은 Google에서 세션을 기록하고, 데이터를 변환하고, 웹브라우저를 통해 실시간으로 제공한 방법을 보여줍니다.

https://g.co/VirtualArtSessions

살기 좋은 시간이야! 가상 현실이 소비자 제품으로 도입되면서 새로운 미지의 가능성이 발견되고 있습니다. HTC Vive에서 제공되는 Google 제품인 Tilt Brush를 사용하면 3차원 공간에 그릴 수 있습니다. Tilt Brush를 처음 사용해 봤을 때, 모션 추적 컨트롤러로 그림을 그릴 때의 느낌과 '초능력이 있는 방에 있음'은 실감 나게 느껴집니다. 주변 텅 빈 공간에 그림을 그릴 수 있는 듯한 경험은 실제로 없습니다.

가상 예술작품

Google의 데이터 아트팀은 Tilt Brush가 아직 작동하지 않는 웹에서 VR 헤드셋이 없는 사람들에게 이러한 경험을 공개해야 한다는 과제를 제시했습니다. 이를 위해 팀은 조각가, 일러스트레이터, 컨셉 디자이너, 패션 아티스트, 설치 예술가, 거리 예술가를 섭외해 이 새로운 매체에서 자신만의 스타일로 예술작품을 만들었습니다.

가상 현실에서 그림 녹화하기

Unity에 내장된 Tilt Brush 소프트웨어 자체는 룸스케일 VR을 사용하여 머리 위치(헤드 마운트 디스플레이(HMD))와 각 손의 컨트롤러를 추적하는 데스크톱 애플리케이션입니다. Tilt Brush에서 만든 아트워크는 기본적으로 .tilt 파일로 내보내집니다. 이러한 경험을 웹에 제공하기 위해서는 아트 데이터뿐만 아니라 다양한 데이터가 필요하다는 사실을 깨달았습니다. Google은 Tilt Brush팀과 긴밀하게 협력하여 Tilt Brush를 수정하여 실행취소/삭제 작업과 아티스트의 머리와 손 위치를 초당 90회 내보냈습니다.

그릴 때 Tilt Brush는 컨트롤러의 위치와 각도를 취하여 시간 경과에 따른 여러 지점을 '획'으로 변환합니다. 여기에서 예를 확인할 수 있습니다. 우리는 이러한 스트로크를 추출하여 원시 JSON으로 출력하는 플러그인을 작성했습니다.

    {
      "metadata": {
        "BrushIndex": [
          "d229d335-c334-495a-a801-660ac8a87360"
        ]
      },
      "actions": [
        {
          "type": "STROKE",
          "time": 12854,
          "data": {
            "id": 0,
            "brush": 0,
            "b_size": 0.081906750798225,
            "color": [
              0.69848710298538,
              0.39136275649071,
              0.211316883564
            ],
            "points": [
              [
                {
                  "t": 12854,
                  "p": 0.25791856646538,
                  "pos": [
                    [
                      1.9832634925842,
                      17.915264129639,
                      8.6014995574951
                    ],
                    [
                      -0.32014992833138,
                      0.82291424274445,
                      -0.41208130121231,
                      -0.22473378479481
                    ]
                  ]
                }, ...many more points
              ]
            ]
          }
        }, ... many more actions
      ]
    }

위의 스니펫은 스케치 JSON 형식의 형식을 간략히 보여줍니다.

여기서 각 획은 'STROKE' 유형의 작업으로 저장됩니다. 획 동작 외에도 아티스트가 스케치 도중에 실수로 실수하고 마음을 바꾸는 것을 보여주길 원했기 때문에 '삭제' 작업을 저장하는 것이 중요했습니다. 이 작업은 전체 획에 대해 삭제 또는 실행취소 작업의 역할을 했습니다.

각 획의 기본 정보가 저장되므로 브러시 유형, 브러시 크기, 색상 RGB가 모두 수집됩니다.

마지막으로 획의 각 꼭짓점이 저장됩니다. 여기에는 위치, 각도, 시간, 컨트롤러의 트리거 압력 강도 (각 지점 내에서 p로 표시됨)가 포함됩니다.

회전은 4개의 구성요소로 된 사원수입니다. 이는 나중에 스트로크를 렌더링할 때 짐벌 잠금을 방지하기 위해 중요합니다.

WebGL로 스케치 재생하기

웹브라우저에서 스케치를 표시하기 위해 THREE.js를 사용하고 Tilt Brush의 내부 기능을 모방하는 도형 생성 코드를 작성했습니다.

Tilt Brush는 사용자의 손 움직임에 따라 실시간으로 삼각형 스트립을 생성하지만, 웹에 표시할 때에는 이미 스케치 전체가 '완료'된 상태입니다. 이렇게 하면 대부분의 실시간 계산을 우회하고 로드 시 도형을 베이킹할 수 있습니다.

WebGL 스케치

획에 있는 각 꼭짓점 쌍은 방향 벡터 (위에 표시된 것처럼 각 점을 연결하는 파란색 선, 아래 코드 스니펫의 moveVector)를 생성합니다. 각 지점에는 컨트롤러의 현재 각도를 나타내는 사원수인 방향도 포함되어 있습니다. 삼각형 스트립을 생성하려면 이러한 각 지점을 반복하여 방향 및 컨트롤러 방향에 수직인 노멀을 생성합니다.

각 획의 삼각형 스트립을 계산하는 프로세스는 Tilt Brush에 사용된 코드와 거의 동일합니다.

const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );

function computeSurfaceFrame( previousRight, moveVector, orientation ){
    const pointerF = V_FORWARD.clone().applyQuaternion( orientation );

    const pointerU = V_UP.clone().applyQuaternion( orientation );

    const crossF = pointerF.clone().cross( moveVector );
    const crossU = pointerU.clone().cross( moveVector );

    const right1 = inDirectionOf( previousRight, crossF );
    const right2 = inDirectionOf( previousRight, crossU );

    right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );

    const newRight = ( right1.clone().add( right2 ) ).normalize();
    const normal = moveVector.clone().cross( newRight );
    return { newRight, normal };
}

function inDirectionOf( desired, v ){
    return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}

획 방향과 방향을 직접 결합하면 수학적으로 모호한 결과가 반환됩니다. 여러 법선이 파생될 수 있으며 도형에 '비틀기'가 발생하는 경우가 많습니다.

획의 지점을 반복할 때는 '선호하는 오른쪽' 벡터를 유지하고 이를 computeSurfaceFrame() 함수에 전달합니다. 이 함수는 획의 방향 (마지막 지점에서 현재 지점까지)과 컨트롤러의 방향 (쿼터니언)을 기준으로 쿼드 스트립의 쿼드를 도출할 수 있는 정상을 제공합니다. 더 중요한 것은 다음 계산 집합을 위한 새로운 '오른쪽 우선' 벡터도 반환한다는 것입니다.

획

각 획의 기준점을 기반으로 쿼드를 생성한 후 쿼드를 한 쿼드에서 다음 쿼드로 보간하여 쿼드를 융합합니다.

function fuseQuads( lastVerts, nextVerts) {
    const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
    const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );

    lastVerts[1].copy( vTopPos );
    lastVerts[4].copy( vTopPos );
    lastVerts[5].copy( vBottomPos );
    nextVerts[0].copy( vTopPos );
    nextVerts[2].copy( vBottomPos );
    nextVerts[3].copy( vBottomPos );
}
융합된 쿼드
통합된 쿼드.

각 쿼드에는 다음 단계로 생성되는 UV도 포함되어 있습니다. 일부 브러시에는 모든 획이 페인트 브러시의 다른 획처럼 느껴지도록 다양한 획 패턴이 포함되어 있습니다. 이는 텍스처 아틀라스를 사용하여 실행됩니다. 여기서 각 브러시 텍스처에는 가능한 모든 변형이 포함됩니다. 획의 UV 값을 수정하여 올바른 텍스처를 선택합니다.

function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
    let fYStart = 0.0;
    let fYEnd = 1.0;

    if( useAtlas ){
    const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
    fYStart = fYWidth * atlasIndex;
    fYEnd = fYWidth * (atlasIndex + 1.0);
    }

    //get length of current segment
    const totalLength = quadLengths.reduce( function( total, length ){
    return total + length;
    }, 0 );

    //then, run back through the last segment and update our UVs
    let currentLength = 0.0;
    quadUVs.forEach( function( uvs, index ){
    const segmentLength = quadLengths[ index ];
    const fXStart = currentLength / totalLength;
    const fXEnd = ( currentLength + segmentLength ) / totalLength;
    currentLength += segmentLength;

    uvs[ 0 ].set( fXStart, fYStart );
    uvs[ 1 ].set( fXEnd, fYStart );
    uvs[ 2 ].set( fXStart, fYEnd );
    uvs[ 3 ].set( fXStart, fYEnd );
    uvs[ 4 ].set( fXEnd, fYStart );
    uvs[ 5 ].set( fXEnd, fYEnd );

    });

}
오일 브러시의 텍스처 아틀라스에 있는 4개의 텍스처
오일 브러시용 텍스처 아틀라스에 있는 텍스처 4개
Tilt Brush에서
Tilt Brush
WebGL에서
WebGL

각 스케치에는 획 수가 무제한이고 획을 런타임에 수정할 필요가 없으므로 획 도형을 미리 미리 계산하여 단일 메시로 병합합니다. 각각의 새 브러시 유형은 자체 머티리얼이어야 하지만 그리기 호출은 브러시당 하나로 줄어듭니다.

위의 전체 스케치가 WebGL에서 한 번의 그리기 호출로 실행됩니다.
위의 전체 스케치를 WebGL에서 한 번의 그리기 호출로 실행합니다.

시스템에 대한 스트레스 테스트를 하기 위해 가능한 한 많은 꼭짓점으로 공간을 채우는 데 20분이 걸리는 스케치를 만들었습니다. 결과 스케치는 WebGL에서 여전히 60fps로 재생되었습니다.

획의 원래 꼭짓점 각각에는 시간도 포함되어 있으므로 데이터를 쉽게 재생할 수 있습니다. 프레임당 획을 다시 계산하는 작업은 매우 느립니다. 따라서 로드 시 전체 스케치를 미리 계산하고 필요할 때 각 쿼드를 간단히 표시했습니다.

쿼드를 숨기는 것은 단순히 꼭짓점을 0,0,0 점으로 접는 것을 의미했습니다. 시간이 쿼드가 표시되어야 하는 시점에 도달하면 꼭짓점의 위치를 제자리에 다시 배치합니다.

셰이더로 GPU의 꼭짓점을 완전히 조작하는 것이 개선의 여지가 있습니다. 현재 구현에서는 현재 타임스탬프에서 꼭짓점 배열을 순환하고 표시해야 하는 꼭짓점을 확인한 후 도형을 업데이트하여 이 함수를 배치합니다. 이로 인해 CPU에 많은 부하가 발생하여 팬이 회전하고 배터리 수명이 낭비됩니다.

가상 예술작품

아티스트 녹화

스케치 자체만으로는 충분하지 않다고 생각했습니다. 우리는 화가들이 스케치 속에서 각 붓놀림을 그리는 것을 보여주고자 했습니다.

아티스트를 촬영하기 위해 우리는 Microsoft Kinect 카메라를 사용하여 우주 속의 아티스트 신체에 관한 깊이 데이터를 기록했습니다. 이렇게 하면 그림이 나타나는 공간과 같은 공간에 3차원 도형을 표시할 수 있습니다.

예술가의 몸이 스스로 가리면 뒤에 무엇이 보이는지 알 수 없으므로, 우리는 방의 반대편에 있는 중앙에서 중앙을 가리키는 이중 Kinect 시스템을 사용했습니다.

깊이 정보 외에도 표준 DSLR 카메라로 장면의 색상 정보도 캡처했습니다. 우리는 훌륭한 DepthKit 소프트웨어를 사용하여 심도 카메라와 컬러 카메라의 영상을 보정하고 병합했습니다. Kinect는 색상을 기록할 수 있지만, 노출 설정을 제어하고 아름다운 고급 렌즈를 사용하며 고화질로 녹화할 수 있기 때문에 DSLR을 사용하기로 했습니다.

영상을 녹화하기 위해 Google은 아티스트인 HTC Vive와 카메라를 수용할 특별한 공간을 지었습니다. 모든 표면은 적외선을 흡수하여 더 깨끗한 포인트 클라우드 (벽의 더베인, 바닥의 골지 고무 매팅)를 제공하는 재료로 덮었습니다. 포인트 클라우드 장면에 머티리얼이 표시되는 경우를 대비해 검은색 머티리얼을 선택했습니다. 그러면 흰색만큼 산만해지지 않도록 하기 위해서입니다.

레코딩 아티스트

그 결과 얻은 동영상 녹화를 통해 입자 시스템을 투영하기에 충분한 정보를 얻을 수 있었습니다. Google은 특히 바닥, 벽, 천장 제거와 같이 영상을 더욱 정리하기 위해 openFrameworks에 몇 가지 추가 도구를 작성했습니다.

녹화된 동영상 세션의 4개 채널 모두 (위에 있는 2개의 색상 채널과 아래에 2개의 심도)
녹화된 동영상 세션의 4개 채널 모두 (위에 있는 2개의 색상 채널과 아래에 2개의 심도)

아티스트를 표시하는 것 외에도 HMD와 컨트롤러도 3D로 렌더링하려고 했습니다. 이 기능은 HMD를 최종 출력에 명확하게 표시하는 데 중요했을 뿐만 아니라 (HTC Vive의 반사 렌즈가 Kinect의 IR 판독값에서 사라짐)을 통해 입자 출력을 디버깅하고 스케치에 동영상을 정렬하기 위한 연락 지점을 제공했습니다.

머리 장착형 디스플레이, 컨트롤러, 입자가 일렬로 정렬되어 있음
헤드 마운트 디스플레이, 컨트롤러, 입자가 정렬됨

이 작업은 각 프레임의 HMD 및 컨트롤러를 추출하는 맞춤 플러그인을 Tilt Brush에 작성하여 수행했습니다. Tilt Brush는 90fps로 실행되기 때문에 엄청난 양의 데이터가 스트리밍되고 스케치의 입력 데이터는 압축되지 않은 상태에서 20MB 이상이었습니다. 또한 이 기법을 사용하여 아티스트가 도구 패널에서 옵션과 미러 위젯의 위치를 선택하는 경우와 같이 일반적인 Tilt Brush 저장 파일에 기록되지 않는 이벤트를 캡처했습니다.

캡처한 4TB의 데이터를 처리하는 과정에서 가장 큰 어려움 중 하나는 모든 다양한 시각적/데이터 소스를 정렬하는 것이었습니다. DSLR 카메라의 각 동영상은 상응하는 Kinect에 맞춰 정렬되어야 픽셀이 공간과 시간에 맞춰 정렬됩니다. 그런 다음 이 두 카메라 장비의 영상을 나란히 맞춰야 한 명의 아티스트가 됩니다. 그런 다음 3D 아티스트를 그림에서 캡처한 데이터에 맞춰 정렬해야 했습니다. 휴! Google에서 이러한 작업의 대부분을 돕기 위해 브라우저 기반 도구를 작성했으며 여기에서 직접 사용해 볼 수 있습니다.

음반 아티스트
데이터를 정렬한 후 NodeJS로 작성된 일부 스크립트를 사용하여 모든 데이터를 처리하고 동영상 파일과 일련의 JSON 파일을 출력했으며 모두 자르고 동기화했습니다. 파일 크기를 줄이기 위해 세 가지 조치를 취했습니다. 먼저, 각 부동 소수점 수의 정밀도를 떨어뜨려서 소수점 이하 최대 3자리의 정밀도를 얻었습니다. 둘째, 지점 수를 30fps로 줄이고 클라이언트 측 위치를 보간했습니다. 마지막으로, 키-값 쌍이 있는 일반 JSON을 사용하는 대신 HMD 및 컨트롤러의 위치 및 회전을 위한 값 순서가 생성되도록 데이터를 직렬화했습니다. 이렇게 하면 파일 크기가 3MB로 줄어 유선을 통해 전달할 수 있습니다.
레코딩 아티스트

동영상 자체는 WebGL 텍스처에서 읽혀 입자가 되는 HTML5 동영상 요소로 제공되므로 동영상 자체는 배경에 숨겨진 상태로 재생되어야 했습니다. 셰이더는 깊이 이미지의 색상을 3D 공간의 위치로 변환합니다. 제임스 조지는 DepthKit에서 바로 영상으로 촬영하는 방법을 보여주는 좋은 예를 공유했습니다.

iOS에는 자동재생되는 웹 동영상 광고로 인해 사용자가 귀찮아하는 것을 방지하기 위해 인라인 동영상 재생에 대한 제한이 있습니다. Google은 웹의 다른 해결 방법과 유사한 기법을 사용했습니다. 이 기법은 동영상 프레임을 캔버스에 복사하고 동영상 탐색 시간을 1/30초마다 수동으로 업데이트합니다.

videoElement.addEventListener( 'timeupdate', function(){
    videoCanvas.paintFrame( videoElement );
});

function loopCanvas(){

    if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){

    const time = Date.now();
    const elapsed = ( time - lastTime ) / 1000;

    if( videoState.playing && elapsed >= ( 1 / 30 ) ){
        videoElement.currentTime = videoElement.currentTime + elapsed;
        lastTime = time;
    }

    }

}

frameLoop.add( loopCanvas );

동영상에서 캔버스로 픽셀 버퍼를 복사하는 작업이 CPU를 많이 사용하기 때문에 이 접근 방식에 따라 iOS 프레임 속도가 크게 낮아지는 부작용이 있었습니다. 이 문제를 해결하기 위해 iPhone 6에서 최소 30fps를 허용하는 동일한 동영상의 더 작은 크기의 버전을 제공했습니다.

결론

2016년 현재 VR 소프트웨어 개발에서 일반적으로 합의된 바는 HMD에서 90fps 이상으로 실행할 수 있도록 도형과 셰이더를 단순하게 유지하는 것입니다. Tilt Brush에서 사용된 기법이 WebGL에 매우 잘 매핑되므로 이는 WebGL 데모의 타겟으로 매우 좋은 것으로 확인되었습니다.

복잡한 3D 메시를 표시하는 웹브라우저는 그 자체만으로는 흥미롭지 않지만, 이는 VR 작업과 웹의 교차 수분이 완전히 가능하다는 개념 증명이었습니다.