Three.js를 사용한 포인트 클라우드 시각화

Nov 28 2022
완벽을 달성하는 방법을 알고 있습니까? Midjourney, Stable Diffusion, 깊이 맵, 얼굴 모핑 및 약간의 3D를 결합하기만 하면 됩니다.
얼마 동안 나는 다른 많은 것 중에서 출력도 포인트 클라우드인 내부 프로젝트를 진행해 왔습니다. 이러한 클라우드의 적절한 시각화 도구에 대해 생각할 때 할 수 있어야 하는 목록을 만들었습니다. 항상 그렇듯이 이 사이드 프로젝트는 시각화만으로 끝나지 않았습니다.
포인트 구름 속의 집

얼마 동안 나는 다른 많은 것 중에서 출력도 포인트 클라우드인 내부 프로젝트를 진행해 왔습니다. 이러한 클라우드의 적절한 시각화 도구에 대해 생각할 때 수행할 수 있어야 하는 작업 목록을 만들었습니다.

  • 커스터마이징
  • 사용자 정의 버텍스/프래그먼트 셰이더를 전달하는 기능
  • 간편한 공유

늘 그렇듯이 이 사이드 프로젝트는 시각화만으로 끝나지 않았습니다. 나는 이것을 다른 사람들에게도 흥미롭게 만들고 싶었기 때문에 다음 15분 동안 깊이 맵/MiDaS, 얼굴 인식/TensorFlow, Beier-Neely 모핑 알고리즘 및 시차 효과에 대해 설명하겠습니다. 걱정하지 마세요. 이미지와 동영상도 많이 있을 것입니다. 이 게시물은 자세한 튜토리얼이 아니라 몇 가지 개념에 대한 쇼케이스이지만 Github 리포지토리와 함께 학습 자료로 충분할 것입니다.

포인트 클라우드 및 Three.js 정보

일반적으로 포인트 클라우드는 공간의 포인트 집합입니다. 이러한 포인트 클라우드는 라이더 또는 최신 iPhone을 통해 얻을 수 있습니다. 탑 라이더는 초당 수백만 개의 포인트를 생성할 수 있으므로 GPU의 최적화 및 사용은 기본적으로 적절한 시각화를 위한 필수 요소입니다. 데이터 세트 크기와 관련된 시각화 접근 방식에 대한 다른 게시물을 작성할 수도 있지만 지금은 Three.js와 WebGL로 충분합니다. 곧 WebGPU 도 사용할 수 있기를 바랍니다 . WebGPU 는 이미 사용 가능하지만 아직 Three.js 측면에서 공식적인 것은 없습니다.

Three.js는 gl.POINTS 플래그로 내부적으로 렌더링되는 Points 3D 객체를 제공합니다 . Points 클래스에는 Material과 Geometry라는 두 가지 매개변수가 필요합니다. 둘 다 자세히 살펴 보겠습니다.

기하학

더 빠른 프로토타입을 만들 수 있는 가능성을 제공하는 Plane of Sphere와 같은 도우미 형상이 많이 있지만 BufferGeometry 를 사용하기로 결정했습니다 . 의미 있는 것을 만드는 데 시간이 더 걸리지만 형상과 관련된 모든 것을 완전히 제어할 수 있습니다. 아래 스니펫을 살펴보겠습니다.

if (this.guiWrapper.textureOptions.textureUsage) {
  geometry.setAttribute('uv', new THREE.BufferAttribute(pointUv, this.UV_NUM))
} else {
  geometry.setAttribute('color', new THREE.BufferAttribute(pointsColors, this.COL_NUM))
}
geometry.setAttribute('position', new THREE.BufferAttribute(pointsToRender, this.POS_NUM))

geometry.morphAttributes.position = []
geometry.morphAttributes.color = []

// Add flat mapping
geometry.morphAttributes.position[0] = new THREE.BufferAttribute(pointsToRenderFlat, this.POS_NUM)
geometry.morphAttributes.color[0] = new THREE.BufferAttribute(pointsColors, this.COL_NUM)

// Add "natural" mapping
geometry.morphAttributes.position[1] = new THREE.BufferAttribute(pointsToRenderMapped, this.POS_NUM)
geometry.morphAttributes.color[1] = new THREE.BufferAttribute(pointsColors, this.COL_NUM)

2D 이미지와 같은 간단한 경우에는 깊이 맵을 변위 맵으로 사용할 수 있지만 사용 사례에서는 포인트 클라우드와 3D 모델의 더 복잡한 동작이 필요합니다.

재료

다시 말하지만 Three.js에는 미리 정의된 재질이 많이 있지만 우리의 경우에는 사용자 지정 셰이더를 제공할 수 있고 더 유연하고 더 나은 성능을 제공할 수 있기 때문에 ShaderMaterial 을 사용했습니다. 다음 스니펫은 기본 버텍스 셰이더와 프래그먼트 셰이더를 모두 보여줍니다. 포함 중 일부는 Three.js 전용이므로 TypeScript 측에서 액세스할 수 있는 API를 활용할 수 있습니다. 그들의 코드는 Github 에서 사용할 수 있으므로 필요한 모든 것을 항상 확인할 수 있습니다.

export const VERTEX_SHADER = `
    #ifdef GL_ES
    precision highp float;
    #endif

    uniform float size;
    uniform float scale;
    #define USE_COLOR
    #define USE_MORPHCOLORS
    #include <common>
    #include <color_pars_vertex>
    #include <morphtarget_pars_vertex>
    
    attribute vec3 color;
    varying vec2 vUv;
    uniform float u_time;
    uniform float u_minZ;
    uniform float u_maxZ;
    uniform float u_scale;
    uniform vec3 u_camera_angle;
    uniform int u_parallax_type;
    
    void main()
    {
        #include <color_vertex>
        #include <begin_vertex>
        #include <morphtarget_vertex>
        vColor = color;
        vUv = uv;
        gl_PointSize = 2.0;

        #if defined( MORPHTARGETS_COUNT )
            vColor = morphTargetBaseInfluence;
            for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {
                if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb morphTargetInfluences[ i ];
            }
        #endif

        if (u_parallax_type == 1) {
            transformed.x += u_scale*u_camera_angle.x*transformed.z*0.05;
            transformed.y += u_scale*u_camera_angle.y*transformed.z*0.02;
        } else if (u_parallax_type == 2) {
            transformed.x += transformed.z*cos(0.5*u_time)*0.01;
            transformed.y += transformed.z*sin(0.5*u_time)*0.005;
        }

        vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
        gl_Position = projectionMatrix * mvPosition;
    }
`;

export const FRAGMENT_SHADER = `
    #ifdef GL_ES
    precision highp float;
    #endif

    varying vec3 vColor;
    varying vec2 vUv;
    uniform sampler2D u_texture;
    uniform bool u_use_texture;

    void main()
    {
        if (u_use_texture) {
            gl_FragColor = texture2D(u_texture, vUv);
        } else {
            gl_FragColor = vec4( vColor, 1.0 );
        }
    }
    
`;

라이브 데모 웹사이트에 관한 중요 참고 사항. 이 게시물에 보이는 움짤과 영상은 비주얼라이저가 raw/HQ 에셋을 사용하면서 만들어졌습니다. 라이브 데모는 약 10배, 총 약 15MB로 축소된 자산을 사용합니다. 또한 로컬 환경에서 원시 데이터를 로드하고 몇 초를 기다릴 수 있는 여유가 있는 동안 라이브 데모는 경험을 개선하기 위해 몇 가지 내부 단축키를 사용하고 있습니다. 즉, 형상 구성은 웹 작업자에게 부분적으로 오프로드되거나 자산을 텍스처로 로드하고 오프스크린 캔버스에 로드하므로 ImageData를 읽고 기본 및 모핑 버퍼를 채울 수 있습니다. — 절박한 시기에는 절박한 조치가 필요합니다. :)

저 구름을 춤추게 하자!

다음 몇 섹션에서는 사용된 개념에 대해 자세히 설명하고 나중에 시도할 수 있는 데모 시나리오를 설명합니다. [이미지 설정 -> 얼굴 인식 -> 데이터 내보내기 -> Three.js 통합]과 같은 파이프라인을 자세히 설명하지는 않겠지만 제공되는 링크를 따라가면 스스로 할 수 있을 것입니다.

내 눈을 봐 — thx Ryska

MiDaS 및 시차 효과

MiDaS 는 단일 이미지에서 깊이 맵을 계산할 수 있는 놀라운 기계 학습 모델입니다(단안 깊이 추정). 여러 데이터 세트에서 학습하여 정말 좋은 결과를 제공하며 장치에서 자유롭게 사용할 수 있습니다. 대부분의 설정은 conda를 통해 이루어지며 원하는 모델(최대 1.3GB)을 다운로드하기만 하면 됩니다. 집에 있는 많은 영리한 사람들의 지식을 쉽게 활용할 수 있습니다. 이미지와 해당 깊이 맵이 있으면 다음 비디오에서 볼 수 있듯이 포인트 클라우드 렌더러에서 사용하는 것을 막을 수 없습니다.

하지만 더 있습니다! Text2Image, Image2Image, Text2Mesh 등의 모델이 유행하고 있으며 뒤처질 수 없습니다. 다음 비디오는 MiDaS를 통해 처리되고 시각화된 몇 가지 AI 생성 이미지(Midjourney 및 Stable Diffusion)를 보여줍니다(이미지가 변경될 때 앞에서 언급한 모핑 버퍼만 교체함).

이미지가 움직인다는 것을 깨달았을 것입니다. 이것이 정점 셰이더의 작업입니다. 시차 효과에는 두 가지 유형이 있습니다. 첫 번째는 카메라 위치를 추적하고 이미지의 중심이 카메라를 직접 응시하도록 구름을 변형하려고 합니다. 이 효과는 꽤 잘 작동하지만 중력이 항상 중앙에 위치하지 않기 때문에 더 정교해야 합니다. 곧 더 나은 접근 방식을 작성할 계획입니다. 아래의 코드 스니펫은 시차 매개변수를 유니폼으로 전달하여 모든 정점이 그에 따라 조정될 수 있도록 하는 것을 보여줍니다. 궁극적으로 이것은 쉐이더로도 옮겨질 것입니다.

//Will be simplified once testing / prototyping is finished
const imgCenterPoint = this.geometryHelper.imgCenterPoints[this.guiWrapper.renderingType.getValue()][this.interpolationOptions.frame]
this.lastCameraPosition = this.camera.position.clone()
let angleX = this.camera.position.clone().setY(0).angleTo(imgCenterPoint)
let angleY = this.camera.position.clone().setX(0).angleTo(imgCenterPoint)

let normalX = new THREE.Plane().setFromCoplanarPoints(new THREE.Vector3(), this.camera.position.clone().setY(0), imgCenterPoint).normal
let normalY = new THREE.Plane().setFromCoplanarPoints(new THREE.Vector3(), this.camera.position.clone().setX(0), imgCenterPoint).normal

this.parallaxUniforms.u_scale.value = 1 + this.currentSceneObject.scale.z
this.parallaxUniforms.u_camera_angle.value = new THREE.Vector3(-angleX*normalX.y, angleY*normalY.x, 0)

이미지 모핑과 코알라

깊이 지도와 AI 생성 이미지는 멋지고 모두 좋지만 우리는 또한 자연을 어느 정도 존중해야 합니다. 두 가지 기능을 가진 코알라라는 시나리오가 있습니다. 두 이미지 사이의 이미지 모핑과 "자연" 이미지 표현. 후자부터 시작합시다. 이 시나리오에서 색상은 z 평면에 24비트 숫자로 매핑된 다음 정규화됩니다. 공간 이미지 표현에는 마법 같은 것이 있습니다. 특히 물체를 느낄 수 있는 가능성을 제공하고 물체 인식을 위한 신경망이 작동하는 이유에 대해 더 가까운 직관을 제공하기 때문입니다. 공간에서 물체의 특성을 거의 느낄 수 있습니다.

모핑과 관련하여 이 시나리오에는 두 개의 서로 다른 코알라와 10개의 모핑 중간 이미지가 있습니다. 슬라이더를 통해 이미지를 탐색하거나 WebGL이 탄력 있는 방식으로 애니메이션하도록 할 수 있습니다. 원본 사이의 이미지는 Beier–Neely 모핑 알고리즘 을 통해 생성되었습니다 . 이 알고리즘의 경우 각 라인이 눈이나 턱과 같은 기능을 나타내는 1–1 라인 세트를 매핑해야 합니다. 다음 섹션에서 알고리즘에 대해 자세히 설명하겠습니다. 아래 비디오에서 10개의 라인 기능만 사용하고 결과를 보고 싶었기 때문에 이상한 조각이 있음을 볼 수 있습니다. 그러나 완벽하지 않더라도 애니메이션 버전은 매우 훌륭합니다. 특히 모핑 스카프는 매우 사이키델릭합니다.

이미지 모핑, 세로 깊이 API 및 waifus

위의 모든 것을 결합하고 더 복잡한 예를 살펴볼 때입니다. 이 섹션에서는 깊이 맵을 사용하여 두 이미지 사이를 보간할 것입니다. 내가 언급했듯이 MiDaS는 일반적으로 훌륭하지만 사람 얼굴의 깊이 맵을 얻으려면 사람 얼굴에 대해 특별히 훈련된 더 자세한 모델이 필요합니다. 마이다스). 올해 초 TensorFlow는 Portrait Depth API 를 선보였으며 MiDaS와 마찬가지로 이 프로젝트도 기기에서 실행할 수 있습니다. 이 섹션에서는 Anne Hathaway와 Olivia Munn을 모핑 대상으로 선택합니다. 드롭다운에서 Waifus라는 섹션을 찾을 수 있습니다. 이유는 묻지 마세요. 모핑하기 전에 포인트 클라우드로 Anne의 깊이 맵을 살펴보겠습니다.

모델이 꽤 좋은 결과를 제공했음을 알 수 있습니다. 사람 얼굴의 실제와 같은 메시를 만들 수는 없지만 볼륨이 있는 치아, 몸통과 다른 수준의 머리, 안면 부조와 같은 멋진 디테일이 있습니다. Anne이 두 번째 턱을 가진 것처럼 보이기 때문에 이미지 결함이 있다고 말할 수 있지만 사실이 아님을 알고 있습니다. Nietzsche가 말했듯이 "이중 턱을 가진 사람은 실패한 삶의 심연을 내려다 보는 사람입니다." 또한 인터넷의 인용문을 절대 신뢰하지 마십시오. :)

Portrait Depth API는 두 단계로 작동합니다. 처음에는 얼굴/머리카락/목 및 기타 얼굴에 가까운 부분을 감지합니다. 이미지의 나머지 부분과 최종적으로 깊이 맵을 생성합니다. 마스킹 단계가 항상 정확한 것은 아니므로 최종 깊이 맵의 가장자리에 날카로운 노이즈가 있습니다. 다행스럽게도 좋은 노이즈입니다. 버퍼에 쓰는 동안 동일한 연습에서 제거할 수 있습니다. 노이즈를 제거하기 위해 휴리스틱을 작성했으며 첫 번째 시도에서 작동했습니다.

removeNoise(depthMapColor: number, depthmap: Array<Array<number>>, i: number, j: number, width: number, height: number, pointPointer: number, pointsToRender: Float32Array) {
        if (depthMapColor != 0) {
            const percentage = depthMapColor/100
            let left = depthMapColor
            let right = depthMapColor
            let top = depthMapColor
            let down = depthMapColor
            const dropThreshold = 5*percentage
    
            if (j > 0) left = depthmap[i][j-1]
            if (j < width-1) right = depthmap[i][j+1]
            if (i > 0) top = depthmap[i-1][j]
            if (i < height-1) down = depthmap[i+1][j]
            
            if(Math.abs(left - depthMapColor) > dropThreshold || Math.abs(right - depthMapColor) > dropThreshold) {
                pointsToRender[pointPointer*3 + 2] = 0
            }
            else if(Math.abs(top - depthMapColor) > dropThreshold || Math.abs(down - depthMapColor) > dropThreshold) {
                pointsToRender[pointPointer*3 + 2] = 0
            } else {
                // extra scale
                pointsToRender[pointPointer*3 + 2] = 3*(1 - depthMapColor)
            }
        }
    }

노이즈 제거 전과 후

코알라를 모핑하면 조각이 생성되기 때문에 얼굴 모핑을 위해 TensorFlow와 코알라의 얼굴/특징 인식 모델을 사용하기로 결정했습니다. 코알라보다 8배 더 많은 약 80개의 특징선을 사용했습니다.

결과는 훨씬 더 좋고 깊이 맵을 사용하는 경우에도 정말 멋져 보입니다! 올리비아의 머리카락에 한 가지 결함이 있지만 나중에 고칠 수 없는 것은 없습니다. 이 모핑에 애니메이션을 적용하면 각 프레임이 픽셀 덩어리를 잘라냅니다. 이는 배경이 평평한 것으로 간주되지만 머리로부터의 거리에 따라 해당 청크를 부드러운 기능 및 가속화된 애니메이션에 매핑하기만 하면 후처리에서 고정될 수도 있기 때문입니다.

일반적인 이미지 모핑

Beier-Neely 모핑 알고리즘과 관련하여 병렬화 또는 GPU 가속 구현을 찾을 수 없었기 때문에 수십 개의 특징선이 있는 2k*2k 픽셀과 같은 큰 이미지에서 중간 프레임을 계산하는 데 많은 시간이 걸립니다. 앞으로는 이러한 구현을 작성할 계획입니다. 첫 번째 구현은 CUDA를 통한 로컬/서버 사용을 위한 것이고 두 번째 구현은 GPGPU 및 셰이더를 활용하는 것입니다. GPGPU는 더 창의적일 수 있기 때문에 특히 흥미롭습니다.

프로세스 속도 향상 또는 전체 프로세스로서 모핑 프로세스에서 신경망을 동반하는 여러 프로젝트(예: DiffMorph )가 있습니다. 좋은 오래된 결정론적 알고리즘을 사용하고 실험하는 이유는 몇 가지 논문을 살펴보고 GPU 관련 구현을 하고 싶었기 때문입니다.

사이드 프로젝트는 훌륭합니다

다른 프로젝트의 포인트 클라우드를 시각화하기 위한 빠른 프로토타입으로 시작했지만 결국에는 재미를 위해 약 일주일 동안 테스트하고 새 기능을 추가했으며 그 과정에서 몇 가지 사항도 배웠습니다. 아래에서 전체 코드가 포함된 내 github 링크와 라이브 데모를 위한 웹사이트 링크를 찾을 수 있습니다. 새로운 아이디어로 자유롭게 분기하거나 PR을 만드십시오. 더 많은 기능을 추가할 계획이지만 더 이상 망치고 싶지는 않습니다. 면책 조항입니다. 저는 웹 개발자가 아니므로 이상한 구성을 사용할 수 있지만 비판에 열려 있습니다!

Github 리포지토리
라이브 웹 데모

프로젝트는 사용자 지정 이미지를 업로드할 수 없는 정적 웹 사이트입니다. 사용자 지정 사용을 위해서는 리포지토리를 복제하고 이미지/깊이 맵을 직접 제공해야 합니다. conda 및 npm을 사용한 적이 있다면 MiDaS 및 기타 도구와 관련된 문제가 없을 것입니다. 프로젝트를 동적으로 만들고 서버 측에서 깊이 맵/모핑/3D 사진을 생성할 수 있지만 시간 가능성에 따라 다릅니다. 하지만 확실히 기능을 추가하고 리팩토링할 계획입니다. 일부 시나리오에서는 Safari 또는 Chrome을 사용하는 동안 iPhone에서 페이지를 다시 로드해야 합니다. 곧 디버그할 예정입니다.

시간을 내어 이 게시물을 읽어주셔서 감사합니다. 곧 봐요!