Visualisasi awan titik dengan Three.js

Nov 28 2022
Apakah Anda tahu bagaimana mencapai kesempurnaan? Cukup gabungkan Midjourney, Stable Diffusion, peta kedalaman, morphing wajah, dan sedikit 3D.
Untuk beberapa waktu saya telah mengerjakan proyek internal yang keluarannya di antara banyak hal lainnya juga merupakan awan titik. Saat saya berpikir tentang alat visualisasi yang tepat untuk awan tersebut, saya membuat daftar hal-hal yang seharusnya dapat dilakukan: Seperti biasa, proyek sampingan ini tidak berakhir hanya dengan visualisasi.
Rumah di awan titik

Untuk beberapa waktu saya telah mengerjakan proyek internal yang keluarannya di antara banyak hal lainnya juga merupakan awan titik. Saat saya berpikir tentang alat visualisasi yang tepat untuk awan tersebut, saya membuat daftar hal-hal yang seharusnya dapat dilakukan:

  • Kustomisasi
  • Kemampuan untuk melewati shader verteks/fragmen khusus
  • Mudah dibagikan

Seperti biasa, proyek sampingan ini tidak berakhir hanya dengan visualisasi. Karena saya ingin membuat ini menarik juga untuk orang lain, dalam 15 menit ke depan saya akan memberi tahu Anda sesuatu tentang peta kedalaman / MiDaS, pengenalan wajah / TensorFlow, algoritme morphing Beier–Neely, dan efek paralaks. Jangan khawatir, akan ada banyak gambar dan video juga. Posting ini bukan tutorial mendetail melainkan sebuah karya dari beberapa konsep, namun seharusnya cukup baik sebagai bahan studi bersama dengan repositori Github.

Tentang point cloud dan Three.js

Secara umum, awan titik hanyalah sekumpulan titik di ruang angkasa. Awan titik tersebut dapat diperoleh melalui ie lidars atau bahkan melalui iPhone modern. Lidar teratas dapat menghasilkan beberapa juta titik per detik sehingga pengoptimalan dan penggunaan GPU pada dasarnya adalah suatu keharusan untuk visualisasi yang tepat. Saya mungkin menulis posting lain tentang pendekatan visualisasi sehubungan dengan ukuran dataset tetapi untuk saat ini Three.js dan WebGL sudah cukup baik. Mudah-mudahan kita akan segera dapat menggunakan WebGPU juga — ini sudah tersedia tetapi sejauh ini belum ada yang resmi dalam hal Three.js.

Three.js memberi kita objek Points 3D yang dirender secara internal dengan flag gl.POINTS . Kelas poin mengharapkan dua parameter, Material dan Geometri. Mari kita lihat lebih dekat keduanya.

Geometri

Ada banyak geometri pembantu seperti Plane of Sphere yang memberi Anda kemungkinan untuk membuat prototipe lebih cepat, namun saya memutuskan untuk menggunakan BufferGeometry . Meskipun Anda membutuhkan lebih banyak waktu untuk membuat sesuatu yang bermakna, Anda memiliki kendali penuh atas segala sesuatu yang berhubungan dengan geometri. Mari kita lihat cuplikan di bawah ini:

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)

Dalam kasus sederhana seperti gambar 2D, Anda dapat menggunakan peta kedalaman sebagai peta perpindahan, tetapi dalam kasus penggunaan saya, saya memerlukan perilaku point cloud dan model 3D yang lebih kompleks.

Bahan

Sekali lagi, ada banyak bahan yang telah ditentukan sebelumnya di Three.js tetapi dalam kasus kami, saya menggunakan ShaderMaterial karena kami dapat menyediakan shader khusus dan memiliki lebih banyak fleksibilitas/kinerja yang lebih baik. Cuplikan berikutnya menampilkan vertex dasar dan shader fragmen. Beberapa penyertaan khusus untuk Three.js sehingga Anda dapat memanfaatkan API yang dapat diakses di sisi TypeScript. Karena kode mereka tersedia di Github , Anda selalu dapat memeriksa apa pun yang diperlukan.

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 );
        }
    }
    
`;

Catatan penting terkait situs web demo langsung. Gif dan video yang terlihat di postingan ini dibuat saat visualizer menggunakan aset mentah / HQ. Demo langsung menggunakan aset yang diperkecil dengan faktor sekitar 10, total sekitar 15MB. Selain itu, saat di env lokal saya mampu memuat data mentah dan menunggu beberapa detik, demo langsung menggunakan beberapa pintasan internal untuk menjadikan pengalaman lebih baik. Yaitu konstruksi geometri sebagian diturunkan ke pekerja Web atau memuat aset sebagai tekstur dan memuatnya ke kanvas di luar layar sehingga saya dapat membaca ImageData dan mengisi buffer dasar dan morphing. — Saat-saat putus asa membutuhkan tindakan putus asa. :]

Ayo buat awan itu menari!

Di beberapa bagian selanjutnya saya akan memberi tahu Anda lebih banyak tentang konsep yang digunakan dan menjelaskan skenario demo yang dapat Anda coba nanti. Saya tidak akan menjelaskan pipeline seperti [Set gambar -> Pengenalan wajah -> Ekspor data -> Integrasi Three.js] secara mendetail, tetapi Anda harus dapat melakukannya sendiri dengan mengikuti tautan yang disediakan.

Tatap mataku — terima kasih Ryska

MiDaS dan efek paralaks

MiDaS adalah model pembelajaran mesin luar biasa yang dapat menghitung peta kedalaman dari satu gambar (Estimasi kedalaman bermata). Dipelajari di banyak kumpulan data, ini memberikan hasil yang sangat bagus dan Anda bebas menggunakannya di perangkat Anda. Sebagian besar penyiapan dilakukan melalui conda, Anda hanya perlu mengunduh model yang diinginkan (hingga 1,3GB). Waktu yang tepat untuk hidup, Anda dapat dengan mudah memanfaatkan pengetahuan dari sekelompok orang pintar di rumah. Setelah Anda memiliki gambar dan peta kedalamannya masing-masing, tidak ada yang menghentikan Anda untuk menggunakannya di point cloud renderer seperti yang Anda lihat di video berikutnya.

Tapi tunggu, masih ada lagi! Model Text2Image, Image2Image, Text2Mesh dll. sedang tren dan kita tidak bisa ketinggalan. Video berikutnya menunjukkan beberapa gambar yang dihasilkan AI (Midjourney dan Stable Diffusion) yang diproses melalui MiDaS dan divisualisasikan (Saat gambar berubah, kami hanya menukar buffer morphing yang disebutkan sebelumnya):

Anda mungkin menyadari bahwa gambar bergerak. Itulah pekerjaan shader vertex kami. Ada dua jenis efek paralaks. Yang pertama adalah melacak posisi kamera dan mencoba mengubah awan sehingga pusat gambar menatap langsung ke kamera. Efek ini bekerja dengan baik tetapi harus lebih canggih karena gravitasi tidak selalu diposisikan di tengah. Saya berencana untuk segera menulis pendekatan yang lebih baik. Cuplikan kode di bawah ini menunjukkan penerusan paralaks params ke seragam sehingga setiap simpul dapat menyesuaikannya. Pada akhirnya ini akan dipindahkan ke shader juga.

//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)

Gambar morphing dan Koala

Peta kedalaman dan gambar yang dihasilkan AI memang keren dan semuanya, tetapi kita juga perlu menghormati alam. Ada skenario bernama Koala dengan dua fitur. Gambar morphing antara dua gambar dan representasi gambar "alam". Mari kita mulai dengan yang terakhir. Dalam skenario ini warna dipetakan ke z-plane sebagai angka 24-bit dan kemudian dinormalisasi. Ada sesuatu yang ajaib tentang representasi citra spasial, terutama karena memberi kita kemungkinan untuk merasakan objek dan juga memberikan intuisi yang lebih dekat tentang mengapa jaringan saraf untuk pengenalan objek bekerja. Anda hampir bisa merasakan karakterisasi objek di luar angkasa:

Mengenai morphing, dalam skenario ini kami memiliki dua koala yang berbeda dan 10 gambar perantara morphing — Anda dapat menjelajahinya melalui penggeser atau membiarkan WebGL menganimasikannya dengan cara goyang. Gambar di antara dokumen asli dihasilkan melalui algoritme morphing Beier–Neely . Untuk algoritme ini, Anda perlu memetakan 1–1 rangkaian garis di mana setiap garis mewakili beberapa fitur seperti mata atau dagu. Saya akan berbicara tentang algoritme secara lebih rinci di bagian selanjutnya. Dalam video di bawah ini Anda dapat melihat ada beberapa fragmen aneh karena saya hanya menggunakan 10 fitur garis dan ingin melihat hasilnya. Tetapi meskipun tidak sempurna, versi animasinya cukup bagus, terutama syal morphing, sangat psychedelic:

Image Morphing, Portrait Depth API, dan waifus

Saatnya menggabungkan semua hal di atas dan melihat contoh yang lebih kompleks. Pada bagian ini kita akan melakukan interpolasi antara dua gambar dengan peta kedalaman. Seperti yang saya sebutkan, MiDaS secara umum luar biasa, tetapi ketika Anda ingin mendapatkan peta kedalaman wajah manusia, Anda memerlukan model yang lebih detail, dilatih secara khusus pada wajah manusia (bukan berarti Anda tidak bisa mendapatkan peta kedalaman yang cukup bagus dari MiDaS). Awal tahun ini TensorFlow menghadirkan Portrait Depth API dan mirip dengan MiDaS, proyek ini juga dapat dijalankan di perangkat Anda. Untuk bagian ini saya memilih Anne Hathaway dan Olivia Munn sebagai target morphing. Di dropdown Anda dapat menemukan bagian ini bernama Waifus. Jangan tanya kenapa. Sebelum morphing, mari kita lihat peta kedalaman Anne sebagai point cloud:

Kita dapat melihat bahwa model tersebut memberikan hasil yang cukup bagus. Kami tidak akan dapat membuat jaring seperti wajah manusia yang sebenarnya, tetapi jika tidak, ada detail yang bagus seperti gigi dengan volume, kepala dengan jelas pada tingkat yang berbeda dari batang tubuh, dan relief wajah juga akurat. Bisa dikatakan ada kesalahan gambar karena sepertinya Anne memiliki dagu kedua tapi kita tahu itu tidak benar. Seperti yang dikatakan Nietzsche, "Hanya orang dengan dagu ganda yang melihat ke bawah ke jurang kehidupan mereka yang gagal." Juga, jangan pernah mempercayai kutipan apa pun di internet. :]

Portrait Depth API bekerja dalam dua langkah. Mula-mula ia mendeteksi wajah/rambut/leher dan bagian lain yang dekat dengan wajah (saya tidak yakin bagaimana bobotnya, kalimat ini hanya berasal dari pengalaman saya saat bermain-main dengan model), kemudian membuat topeng hitam ke sisa gambar dan akhirnya menghasilkan peta kedalaman. Langkah masking tidak selalu akurat sehingga peta kedalaman akhir memiliki noise tajam di tepinya. Untungnya ini adalah noise yang bagus — ini dapat dihapus dengan langkah yang sama saat menulis ke buffer. Saya menulis heuristik untuk menghilangkan kebisingan dan berhasil pada percobaan pertama.

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)
            }
        }
    }

Sebelum dan sesudah denoising

Karena morphing koala menghasilkan fragmen, untuk morphing wajah saya memutuskan untuk menggunakan TensorFlow dan model pengenalan wajah / fitur mereka. Saya menggunakan sekitar 80 baris fitur, 8 kali lebih banyak daripada untuk koala:

Hasilnya jauh lebih baik dan terlihat sangat keren, bahkan saat menggunakan peta kedalaman! Ada satu kesalahan pada rambut Olivia tapi tidak ada yang tidak bisa diperbaiki nanti. Saat Anda menganimasikan morphing ini, setiap frame memotong potongan piksel. Ini karena latar belakang dianggap datar tetapi juga dapat diperbaiki dalam pasca-pemrosesan, hanya memetakan potongan tersebut untuk memuluskan fungsi dan animasi yang dipercepat sehubungan dengan jarak dari kepala.

Image Morphing secara umum

Mengenai algoritme morphing Beier–Neely, saya tidak dapat menemukan implementasi yang diparalelkan atau dipercepat GPU sehingga menghitung bingkai menengah pada gambar besar seperti 2k*2k piksel dengan puluhan baris fitur membutuhkan banyak waktu. Di masa depan, saya berencana menulis implementasi ini. Implementasi pertama adalah untuk penggunaan lokal/server melalui CUDA dan yang kedua akan menggunakan GPGPU dan shader. GPGPU sangat menarik karena Anda bisa lebih kreatif.

Ada beberapa proyek (yaitu DiffMorph ) yang menyertai jaringan saraf dalam proses morphing — sebagai percepatan proses atau sebagai proses yang lengkap. Alasan di balik penggunaan dan percobaan dengan algoritme deterministik lama yang bagus adalah karena saya ingin melihat beberapa makalah dan melakukan implementasi terkait GPU.

Proyek sampingan sangat bagus

Ini dimulai sebagai prototipe cepat hanya untuk memvisualisasikan awan titik dari proyek saya yang berbeda tetapi pada akhirnya saya menghabiskan waktu sekitar satu minggu untuk menguji dan menambahkan fitur baru hanya untuk bersenang-senang dan saya juga mempelajari beberapa hal dalam prosesnya. Di bawah ini Anda dapat menemukan tautan ke github saya dengan kode lengkap dan ke situs web untuk demo langsung. Jangan ragu untuk bercabang atau bahkan membuat PR dengan ide-ide baru. Saya berencana untuk menambahkan lebih banyak fitur tetapi saya tidak ingin merusak lebih banyak. Hanya penafian, saya bukan pengembang web jadi saya mungkin menggunakan beberapa konstruksi aneh tapi saya terbuka untuk kritik!

Repositori Github
Demo web langsung

Project adalah situs web statis tanpa kemungkinan untuk mengunggah gambar khusus. Untuk penggunaan khusus, Anda perlu mengkloning repositori dan menyediakan gambar/peta kedalaman sendiri. Jika Anda pernah menggunakan conda dan npm, Anda seharusnya tidak memiliki masalah terkait MiDaS dan alat lainnya. Saya mungkin membuat proyek ini dinamis dan menghasilkan peta kedalaman / morphing / foto 3D di sisi server tetapi itu tergantung pada kemungkinan waktu saya. Tapi saya pasti berencana menambahkan fitur dan refactoring. Beberapa skenario menyebabkan pemuatan ulang halaman di iPhone saat menggunakan Safari atau Chrome, Akan segera di-debug.

Terima kasih atas waktunya dan telah membaca postingan ini. Sampai berjumpa lagi!