Mesin 3D di dalam query SQL

Beberapa tahun yang lalu, forum SQL.ru memutuskan untuk membandingkan implementasi ray tracer dalam berbagai bahasa pemrograman. Sayangnya, aplikasi saya tidak dapat berpartisipasi karena itu tidak menampilkan kata "PIXAR", jadi saya menerbitkannya di sini.

Untuk kemurnian percobaan, saya menggunakan SQLite tanpa ekstensi. Ternyata bahkan tidak ada fungsi SQRT di sana.

WITH RECURSIVE numbers AS (SELECT 0 AS n UNION ALL SELECT n+1 FROM numbers WHERE n<89), pixels AS (SELECT rows.n as row, cols.n as col FROM numbers as rows CROSS JOIN numbers as cols WHERE rows.n > 4 AND rows.n < 38 AND cols.n > 9 AND cols.n < 89), rawRays AS (SELECT row, col, -0.9049 + col * 0.0065 + row * 0.0057 as x, -0.1487 + row * -0.0171 as y, 0.6713 + col * 0.0045 + row * -0.0081 as z FROM pixels), norms AS (SELECT row, col, x, y, z, (1 + x * x + y * y + z * z) / 2 as n FROM rawRays), rays AS (SELECT row, col, x / n AS x, y / n AS y, z / n AS z FROM norms), iters AS (SELECT row, col, 0 as it, 0 as v FROM rays UNION ALL SELECT rays.row, rays.col, it + 1 AS it, v + MAX(ABS(0.7+v*x) - 0.3, ABS(0.7+v*y) - 0.3, ABS(-1.1+v*z) - 0.3, -((0.7+v*x) * (0.7+v*x) + (0.7+v*y) * (0.7+v*y) + (-1.1+v*z) * (-1.1+v*z)) * 1.78 + 0.28) AS v FROM iters JOIN rays ON rays.row = iters.row AND rays.col = iters.col WHERE it < 15), lastIters AS (SELECT it0.row, it0.col, it0.v AS v0, it1.v AS v1, it2.v AS v2 FROM iters as it0 JOIN iters AS it1 ON it0.col = it1.col AND it0.row = it1.row JOIN iters AS it2 ON it0.col = it2.col AND it0.row = it2.row WHERE it0.it = 15 AND it1.it = 14 AND it2.it = 13), res AS (SELECT col, (v0 - v1) / (v1 - v2) as v FROM lastIters) SELECT group_concat( substr('$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. ', round(1 + max(0, min(66, v * 67))), 1) || CASE WHEN col=88 THEN X'0A' ELSE '' END, '') FROM res; 



Di sini Anda dapat memutar kubus

Di bawah analisis permintaan baris demi baris. Seperti biasa, pengetahuan tentang dasar-dasar SQL dan matematika sekolah sudah cukup.


Penafian: Saya jauh dari dunia basis data, jadi saya akan tepat waktu untuk komentar di PM.

Version for Postgres (UPD: berkat flag, ia menjadi lebih cepat dengan urutan besarnya, UPD2: sejumlah perbaikan, sekarang runtime 150ms)
Terima kasih kepada XareH untuk mengoptimalkan permintaan.
 SET ENABLE_NESTLOOP TO OFF; WITH RECURSIVE numbers AS (SELECT n FROM generate_series(0,89) gs(n) ), pixels AS (SELECT rows.n as row, cols.n as col FROM numbers as rows CROSS JOIN numbers as cols WHERE rows.n > 4 AND rows.n < 38 AND cols.n > 9 AND cols.n < 89), rawRays AS (SELECT row, col, -0.9049::double precision + col * 0.0065 ::double precision + row * 0.0057::double precision as x, -0.1487::double precision + row * -0.0171::double precision as y, 0.6713::double precision + col * 0.0045::double precision + row * -0.0081::double precision as z FROM pixels), norms AS (SELECT row, col, x, y, z, (1 + x * x + y * y + z * z) / 2 as n FROM rawRays), rays AS (SELECT row, col, x / n AS x, y / n AS y, z / n AS z FROM norms), iters AS (SELECT row, col, 0 as it, 0.0::double precision as v FROM rays UNION ALL SELECT rays.row, rays.col, it + 1 AS it, v + GREATEST(ABS(0.7 +v*x) - 0.3 , ABS(0.7 +v*y) - 0.3 , ABS(-1.1 +v*z) - 0.3 , -(0.28 + ((0.7 +v*x) * (0.7 +v*x) + (0.7 +v*y) * (0.7 +v*y) + (-1.1 +v*z) * (-1.1 +v*z)) / 0.28 ) / 2.0 + 0.42 ) AS v FROM iters JOIN rays ON rays.row = iters.row AND rays.col = iters.col WHERE it < 15), lastIters AS (SELECT it0.row, it0.col, it0.v AS v0, it1.v AS v1, it2.v AS v2 FROM iters as it0 JOIN iters AS it1 ON it0.col = it1.col AND it0.row = it1.row JOIN iters AS it2 ON it0.col = it2.col AND it0.row = it2.row WHERE it0.it = 15 AND it1.it = 14 AND it2.it = 13), res AS (SELECT row,col, (v0 - v1) / (v1 - v2) as v FROM lastIters) SELECT string_agg(substring('$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. '::text FROM round(1 + GREATEST(0, LEAST(66, v * 67)))::integer FOR 1) || CASE WHEN col=88 THEN E'\n' ELSE '' END, ''::text order by row,col) FROM res; SET ENABLE_NESTLOOP TO ON; 


Untuk memahami terminologi dan prinsip algoritma, disarankan untuk membaca artikel tentang ray marching untuk Excel .

Struktur umum


Daftar tabel pementasan:

  • numbers (n) - berisi angka dari 0 hingga 89.
  • pixels (row, col) - berisi nomor baris dan kolom untuk setiap "piksel".
  • rawRays (row, col, x, y, z) - berisi sinar abnormal dari kamera ke layar .
  • norms (row, col, x, y, z, n) - berisi panjang sinar.
  • rays (row, col, x, y, z) - berisi sinar yang dinormalisasi dari kamera ke layar .
  • iters (row, col, it, v) - berisi iterasi ray marching.
  • lastIters (row, col, v0, v1, v2) - berisi tiga iterasi terakhir dari tabel sebelumnya untuk setiap "pixel".
  • res (col, v) - berisi "kecerahan" piksel.


Mengenai bagaimana mereka saling bergantung, semuanya sederhana: setiap tabel berikutnya hanya menggunakan yang sebelumnya, dan permintaan akhir hanya menggunakan tabel res .

Semua tabel (kecuali numbers dan numbers ) berisi 81 x 29 baris (satu untuk setiap "pixel"), row dan col kolom mengindeks koordinat mereka. Tabel iters berisi 81 x 29 x 15 baris (satu untuk setiap iterasi marching sinar untuk setiap "pixel"). Nomor iterasi ada di kolomnya.

Kueri akhir menghasilkan tabel satu baris dan kolom dengan teks, semua tabel lainnya hanya berisi bilangan real ( row kolom, col dan it bilangan bulat non-negatif).

Ternyata, jika kita menghilangkan tabel tambahan, struktur kueri yang sangat sederhana:

 WITH RECURSIVE numbers AS (SELECT ...), pixels AS (SELECT ...), rawRays AS (SELECT ...), normsSq AS (SELECT ...), norms AS (SELECT ...), rays AS (SELECT ...), iters AS (SELECT ...), lastIters AS (SELECT ...), res AS (SELECT ...) SELECT group_concat(substr('$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. ', round(1 + max(0, min(66, v * 67))), 1) || CASE WHEN col=88 THEN X'0A' ELSE '' END, '') FROM res; 

Kueri Rekursif


Berikut adalah cara standar untuk mendapatkan tabel yang berisi angka dari 0 hingga 89:

 WITH RECURSIVE numbers AS ( SELECT 0 AS n UNION ALL SELECT n+1 FROM numbers WHERE n<89 ) ... 

  • Kueri rekursif hanya berfungsi dalam konstruk WITH . Perhatikan bahwa nama yang diberikan ke tabel digunakan dalam definisinya.
  • SELECT 0 as n adalah baris di mana permintaan rekursif dimulai.
  • UNION ALL berarti bahwa semua baris yang dikembalikan sebagai hasilnya disatukan ke dalam satu tabel tanpa pemeriksaan tambahan. Jika Anda hanya menulis UNION , semua duplikat akan dihapus.
  • SELECT n+1 FROM numbers WHERE n<80 . Nuansa penting di sini adalah bahwa tabel numbers selalu berisi satu baris dengan angka sebelumnya. Pada titik tertentu, kondisi di WHERE memotongnya dan kueri akan berhenti dieksekusi. Hanya setelah itu, semua status tabel sebelumnya akan terhubung oleh operasi UNION ALL .

Ekstrak akar kuadrat


Kami akan menggunakan metode Heron (metode Babel) untuk menghitung root. Katakanlah kita ingin menghitung  sqrtS. Kami sedang membangun seri x0,x1, ldotssesuai dengan rumus berikut:

xn+1= fracxn+ fracSxn2



Logika metode ini sangat sederhana:  sqrtSselalu ada di antara xdan  fracSxuntuk nomor berapa pun x. Oleh karena itu, wajar untuk mengambil tengah segmen antara angka-angka ini sebagai perkiraan.

Secara geometris, ini dapat direpresentasikan sebagai berikut:



Setiap nilai berikutnya membawa root lebih dekat dan lebih dekat, dalam satu langkah kesalahan berkurang setidaknya dua kali.

Nilai awal x0dapat berupa angka positif, misalnya 1. Dalam permainan Gempa, konstanta ajaib 0x5f3759df digunakan untuk ini (lebih tepatnya, itu digunakan untuk akar kuadrat terbalik, namun, metode serupa dapat ditemukan untuk root biasa), tetapi, sayangnya, di SQL ada akses ke representasi biner nomor floating point.

Dalam artikel ini, root diperlukan di dua tempat:

  • ketika menormalkan vektor yang meninggalkan kamera: ray marching sangat tergantung pada jarak, dan untuk menunda mereka, Anda memerlukan vektor dengan panjang 1.
  • ketika menghitung jarak ke batas bola yang dipotong dari kotak.


Dalam kasus pertama, nilainya berada dalam kisaran sempit [0,7,1,5]dan perkiraan awal 1 sempurna. Dalam kasus kedua, mengumpulkan statistik panggilan, saya mendapat nilai rata-rata 0,28yang diambil sebagai yang mulai.

Ternyata dengan nilai awal yang benar , satu iterasi sudah cukup ! Artinya, dalam kasus kami, root diperkirakan dengan fungsi linier:

sqrt1(x)= frac1+x2


sqrt2(x)= frac0.28+ fracx0.282=0.14+1.78x



Hitung sinar dari kamera


Tugas dari empat tabel pertama adalah untuk mengasosiasikan setiap "pixel" dengan vektor tiga dimensi panjang 1 yang keluar dari kamera dan melewati titik yang sesuai pada layar .

Pertama, Anda perlu mendapatkan tabel dengan struktur yang diinginkan, yaitu, dengan sel yang menunjukkan nomor baris dan nomor kolom. Untuk ini, produk Cartesian dari serangkaian angka dari 0 hingga 89 diambil dan baris dan kolom yang kosong dipotong darinya:

 ... pixels AS ( SELECT rows.n as row, cols.n as col FROM numbers as rows CROSS JOIN numbers as cols WHERE rows.n >= 5 AND rows.n < 38 AND cols.n >= 10 AND cols.n < 89 ), ... 

Selanjutnya kita menemukan vektor yang tidak dinormalisasi. Secara umum, mereka memiliki rumus panjang fungsi trigonometri. Agar tidak mempersulit permintaan, saya memperbaiki kamera dan menghitung koefisien:

 ... rawRays AS ( SELECT row, col, -0.9049 + col * 0.0065 + row * 0.0057 as x, -0.1487 + row * -0.0171 as y, 0.6713 + col * 0.0045 + row * -0.0081 as z FROM pixels ), ... 

Setelah itu, kita harus menghitung panjang (perkiraan) vektor-vektor ini dengan rumus sqrt1:

 ... norms AS ( SELECT row, col, x, y, z, (1 + x * x + y * y + z * z) / 2.0 AS n FROM rawRays ), ... 

Tetap membagi koordinat vektor dengan panjangnya untuk mendapatkan vektor dengan panjang 1:

 ... rays AS (SELECT row, col, x / n AS x, y / n AS y, z / n AS z FROM norms), ... 

Iterasi sinar berbaris


Ini menggunakan konstruksi kueri rekursif yang sedikit lebih kompleks yang berisi JOIN . Kami ingin melakukan 15 iterasi dari algoritma marching ray untuk setiap piksel. Jika selama perhitungan rekursif tabel numbers setiap kali tabel berisi satu baris, yang kemudian digabungkan, di sini tabel perantara berisi 81 x 29 baris dan dihitung 15 kali.

Semua geometri tiga dimensi terkandung dalam rumus

SDF beginpmatrixxyz endpmatrix= maks kiri(|x|βˆ’0,3,|y|βˆ’0,3,|z|βˆ’0,3,βˆ’ kiri(sqrt2(x2+y2+z2)βˆ’0,42 kanan) kanan)


  • fungsi  maksberarti persimpangan
  • |x|βˆ’0,3,|y|βˆ’0,3,|z|βˆ’0,3tentukan tiga pasang bidang setengah membentuk kubus dengan sisi 0,3 cdot2
  • βˆ’ kiri(sqrt2(x2+y2+z2)βˆ’0,42 kanan)- bagian luar dari lingkup jari-jari 0,42. Jari-jari diambil lebih besar daripada yang terlihat untuk mengkompensasi ketidakakuratan perkiraan akar kuadrat.


Selanjutnya, kita hanya perlu menghitung urutannya 0=v0,v1,v2 ldots,v15untuk setiap piksel sesuai dengan rumus:

vn+1=vn+SDF kiri( beginpmatrixcamXcamYcamZ endpmatrix+vn beginpmatrixxyz endpmatrix benar)


dimana x,y,z- koordinat vektor yang dinormalisasi. Karena koordinat kamera diulang beberapa kali, saya membulatkannya ke satu tempat desimal.

 ... iters AS ( SELECT row, col, 0 as it, 0 as v FROM rays UNION ALL SELECT rays.row, rays.col, it + 1 AS it, v + MAX( ABS(0.7+v*x) - 0.3, ABS(0.7+v*y) - 0.3, ABS(-1.1+v*z) - 0.3, -( (0.7+v*x) * (0.7+v*x) + (0.7+v*y) * (0.7+v*y) + (-1.1+v*z) * (-1.1+v*z) ) * 1.78 + 0.28 ) AS v FROM iters JOIN rays ON rays.row = iters.row AND rays.col = iters.col WHERE it < 15 ), ... 

Dapatkan intensitas "piksel"


Di sini kami menggunakan rumus yang sama seperti di Excel, yang mendekati komponen difus dari Phong shading:

intensitas= maks kiri(0, min kiri(1, fracv15βˆ’v14v14βˆ’v13 kanan) kanan)



Untuk menghitungnya, Anda harus terlebih dahulu membuat tabel dengan tiga iterasi marching ray terakhir:

 ... lastIters AS ( SELECT it0.row, it0.col, it0.v AS v0, it1.v AS v1, it2.v AS v2 FROM iters as it0 JOIN iters AS it1 ON it0.col = it1.col AND it0.row = it1.row JOIN iters AS it2 ON it0.col = it2.col AND it0.row = it2.row WHERE it0.it = 15 AND it1.it = 14 AND it2.it = 13 ), ... 

Dan, pada kenyataannya, formula itu sendiri (operasi  maksdan  minakan diterapkan dalam permintaan akhir):

 ... res AS (SELECT row, col, (v0 - v1) / (v1 - v2) as v FROM lastIters) ... 

Hasilkan seni ascii


 ... SELECT group_concat( substr( '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. ', round(1 + max(0, min(66, v * 67))), 1 ) || CASE WHEN col=88 THEN X'0A' ELSE '' END, '') FROM res; 

Tugas kueri terakhir adalah mengubah tabel dengan intensitas piksel menjadi satu baris ascii. Pada input, hanya menerima tabel res berisi col dan v .

  • group_concat(s, delim) adalah fungsi agregasi yang menggabungkan ekspresi s untuk semua string, menggunakan string delim sebagai pembatas.
  • CASE WHEN cond1 THEN val1 WHEN cond2 THEN val2 ... ELSE valN END - konstruksi kondisional, analog dengan operator ternary.
  • X'0A' adalah karakter X'0A' baris yang dimasukkan sebelum karakter pertama dari setiap baris.
  • || - operator rangkaian string.
  • substr(s, start, count) - fungsi yang mengembalikan count karakter string s , dimulai dengan karakter dengan angka start . Pengindeksan karakter berasal dari satu.
  • '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. ' Adalah string yang berisi" gradien "dari hitam ( $ ) ke "Putih" (spasi) dalam ascii-karakter. Diambil dari situs http://paulbourke.net/dataformats/asciiart/ .
  • round(1 + max(0, min(66, v * 67))) - mengonversi bilangan real dari interval [0,1]menjadi bilangan bulat dalam jangkauan [1,67]untuk mengambil karakter dengan nomor yang sesuai.

Source: https://habr.com/ru/post/id435390/


All Articles