Moteur 3D dans la requĂȘte SQL

Il y a quelques années, le forum SQL.ru a décidé de comparer les implémentations de ray tracer dans différents langages de programmation. Malheureusement, ma candidature ne peut pas participer car il n'affiche pas le mot "PIXAR", donc je le publie ici.

Pour la puretĂ© de l'expĂ©rience, j'ai utilisĂ© SQLite sans extensions. Il s'est avĂ©rĂ© qu'il n'y avait mĂȘme pas de fonction SQRT lĂ -bas.

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; 



Ici, vous pouvez faire tourner le cube

Analyse kat par ligne de la demande. Comme d'habitude, la connaissance des bases du SQL et des mathématiques scolaires est suffisante.


Avertissement: Je suis loin du monde de la base de données, je serai donc à temps pour les commentaires en PM.

Version pour Postgres (UPD: grùce aux indicateurs, elle est devenue plus rapide d'un ordre de grandeur, UPD2: un certain nombre d'améliorations, maintenant le temps d'exécution est de 150 ms)
Merci à XareH d' avoir optimisé la demande.
 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; 


Pour comprendre la terminologie et le principe de l'algorithme, il est recommandé de lire l' article sur la marche des rayons pour Excel .

Structure générale


Liste des tables intermédiaires:

  • numbers (n) - contient des nombres de 0 Ă  89.
  • pixels (row, col) - contient le numĂ©ro de ligne et de colonne pour chaque "pixel".
  • rawRays (row, col, x, y, z) - contient des rayons anormaux de la camĂ©ra Ă  l' Ă©cran .
  • norms (row, col, x, y, z, n) - contient les longueurs des rayons.
  • rays (row, col, x, y, z) - contient des rayons normalisĂ©s de la camĂ©ra Ă  l' Ă©cran .
  • iters (row, col, it, v) - contient des itĂ©rations de dĂ©filement des rayons.
  • lastIters (row, col, v0, v1, v2) - contient les trois derniĂšres itĂ©rations du tableau prĂ©cĂ©dent pour chaque "pixel".
  • res (col, v) - contient la "luminositĂ©" des pixels.


En ce qui concerne la façon dont ils dĂ©pendent les uns des autres, tout est simple: chaque table suivante utilise uniquement la prĂ©cĂ©dente et la requĂȘte finale utilise uniquement la table res .

Tous les tableaux (à l'exception des numbers et des iters ) contiennent 81 x 29 lignes (une pour chaque «pixel»), les colonnes de row et de colonnes indexent leurs coordonnées. Le tableau des iters contient 81 x 29 x 15 lignes (une pour chaque itération de iters rayons pour chaque «pixel»). Le numéro d'itération est dans la colonne it .

La requĂȘte finale produit un tableau d'une ligne et des colonnes avec du texte, tous les autres tableaux ne contiennent que des nombres rĂ©els (la row colonnes, col et it sont des entiers non nĂ©gatifs).

Il s'avĂšre, si nous omettons les tables auxiliaires, une structure de requĂȘte trĂšs simple:

 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; 

RequĂȘtes rĂ©cursives


Voici une façon standard d'obtenir un tableau contenant des nombres de 0 à 89:

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

  • Les requĂȘtes rĂ©cursives fonctionnent uniquement dans la construction WITH . Notez que le nom donnĂ© Ă  la table est utilisĂ© dans sa dĂ©finition.
  • SELECT 0 as n est la ligne Ă  laquelle la requĂȘte rĂ©cursive commence.
  • UNION ALL signifie que toutes les lignes qui sont retournĂ©es en consĂ©quence sont concatĂ©nĂ©es dans une table sans vĂ©rifications supplĂ©mentaires. Si vous Ă©crivez simplement UNION , tous les doublons seront supprimĂ©s.
  • SELECT n+1 FROM numbers WHERE n<80 . Une nuance importante ici est que la table des numbers contient toujours une ligne avec le numĂ©ro prĂ©cĂ©dent. À un certain point, la condition dans WHERE tronquera et la requĂȘte cessera de s'exĂ©cuter. Ce n'est qu'aprĂšs cela que tous les Ă©tats de table prĂ©cĂ©dents seront connectĂ©s par l'opĂ©ration UNION ALL .

Extraire la racine carrée


Nous utiliserons la mĂ©thode Heron (mĂ©thode babylonienne) pour calculer la racine. Disons que nous voulons calculer  sqrtS . Nous construisons une sĂ©rie x0,x1, ldots selon la formule suivante:

xn+1= fracxn+ fracSxn2



La logique de la mĂ©thode est trĂšs simple:  sqrtS se situe toujours entre x et  fracSx pour n'importe quel nombre x . Par consĂ©quent, il est naturel de prendre le milieu du segment entre ces nombres comme approximation.

GĂ©omĂ©triquement, cela peut ĂȘtre reprĂ©sentĂ© comme suit:



Chaque valeur suivante rapproche la racine de plus en plus, en une étape l'erreur diminue au moins deux fois.

Valeur initiale x0 peut ĂȘtre n'importe quel nombre positif, par exemple 1. Dans le jeu Quake, la constante magique 0x5f3759df a Ă©tĂ© utilisĂ©e pour cela (plus prĂ©cisĂ©ment, elle a Ă©tĂ© utilisĂ©e pour la racine carrĂ©e inversĂ©e, nĂ©anmoins, une mĂ©thode similaire peut ĂȘtre inventĂ©e pour la racine habituelle), mais, malheureusement, il n'y a pas de SQL accĂšs Ă  la reprĂ©sentation binaire des nombres Ă  virgule flottante.

Dans cet article, la racine est nécessaire à deux endroits:

  • lors de la normalisation des vecteurs quittant la camĂ©ra: la marche des rayons dĂ©pend fortement des distances, et pour les reporter, vous avez besoin d'un vecteur de longueur 1.
  • lors du calcul de la distance Ă  la limite d'une sphĂšre dĂ©coupĂ©e dans un carrĂ©.


Dans le premier cas, les valeurs sont dans une plage étroite [0,7,1,5] et une approximation initiale de 1 est parfaite. Dans le deuxiÚme cas, en collectant des statistiques sur les appels, j'ai obtenu la valeur moyenne 0,28 $ qui a été pris comme point de départ.

Il s'est avéré qu'avec la valeur initiale correcte , une itération suffit ! Autrement dit, dans notre cas, la racine est approximée par une fonction linéaire:

sqrt1(x)= frac1+x2


sqrt2(x)= frac0,28+ fracx0,282=0,14+1,78x



Calculez les rayons de la caméra


La tùche des quatre premiers tableaux est d'associer chaque «pixel» à un vecteur tridimensionnel de longueur 1 sortant de la caméra et passant par le point correspondant sur l' écran .

Vous devez d'abord obtenir un tableau avec la structure souhaitée, c'est-à-dire avec des cellules pour lesquelles le numéro de ligne et le numéro de colonne sont indiqués. Pour cela, le produit cartésien d'un ensemble de nombres de 0 à 89 est pris et des lignes et colonnes vides en sont coupées:

 ... 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 ), ... 

Ensuite, nous trouvons les vecteurs non normalisés. En général, ils ont une longue formule de fonctions trigonométriques. Afin de ne pas compliquer la demande, j'ai réparé la caméra et calculé les coefficients:

 ... 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 ), ... 

AprĂšs cela, nous devons calculer les longueurs (approximatives) de ces vecteurs par la formule sqrt1 :

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

Il reste à diviser les coordonnées des vecteurs par leur longueur pour obtenir des vecteurs de longueur 1:

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

Itération des rayons marchant


Il utilise une construction de requĂȘte rĂ©cursive lĂ©gĂšrement plus complexe contenant JOIN . Nous voulons faire 15 itĂ©rations de l'algorithme de marche de rayons pour chaque pixel. Si lors du calcul rĂ©cursif de la table des numbers chaque fois la table contenait une ligne, qui ont ensuite Ă©tĂ© combinĂ©es, ici les tables intermĂ©diaires contiennent 81 x 29 lignes et sont calculĂ©es 15 fois.

Toute la géométrie tridimensionnelle est contenue dans la formule

SDF beginpmatrixxyz endpmatrix= max left(|x|−0.3,|y|−0.3,|z|−0.3,− left(sqrt2(x2+y2+z2)−0,42 droite) droite)


  • fonction  max signifie intersection
  • |x|−0,3,|y|−0,3,|z|−0,3 dĂ©finir trois paires de demi-plans formant un cube avec cĂŽtĂ© 0,3 $ \ cdot 2 $
  • − gauche(sqrt2(x2+y2+z2)−0,42 droite) - la partie extĂ©rieure de la sphĂšre de rayon 0,42 $ . Le rayon est pris plus grand que le visible pour compenser l'inexactitude de l'approximation de la racine carrĂ©e.


Ensuite, nous avons juste besoin de calculer la sĂ©quence 0=v0,v1,v2 ldots,v15 pour chaque pixel selon la formule:

vn+1=vn+SDF left( beginpmatrixcamXcamYcamZ endpmatrix+vn beginpmatrixxyz endpmatrix droite)


oĂč x,y,z - coordonnĂ©es du vecteur normalisĂ©. Étant donnĂ© que les coordonnĂ©es de la camĂ©ra sont rĂ©pĂ©tĂ©es plusieurs fois, je les ai arrondies Ă  une dĂ©cimale.

 ... 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 ), ... 

Obtenez l'intensité des "pixels"


Ici, nous utilisons la mĂȘme formule que dans Excel, qui se rapproche du composant diffus de l'ombrage Phong:

intensitĂ©= max left(0, min left(1, fracv15−v14v14−v13 right) right)

Ă©



Pour le calculer, vous devez d'abord faire un tableau avec les trois derniÚres itérations de la marche des rayons:

 ... 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 ), ... 

Et, en fait, la formule elle-mĂȘme (opĂ©rations  max et  min sera appliquĂ© dans la demande finale):

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

Générer de l'art 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; 

La tĂąche de la requĂȘte finale consiste Ă  convertir une table avec des intensitĂ©s de pixels en une seule ligne ascii. En entrĂ©e, il ne reçoit que la table res contenant les colonnes col et v .

  • group_concat(s, delim) est une fonction d'agrĂ©gation qui concatĂšne l'expression s pour toutes les chaĂźnes, en utilisant la chaĂźne delim comme dĂ©limiteur.
  • CASE WHEN cond1 THEN val1 WHEN cond2 THEN val2 ... ELSE valN END - construction conditionnelle, analogue de l'opĂ©rateur ternaire.
  • X'0A' est le caractĂšre de X'0A' ligne qui est insĂ©rĂ© avant le premier caractĂšre de chaque ligne.
  • || - opĂ©rateur de concatĂ©nation de chaĂźnes.
  • substr(s, start, count) - une fonction qui renvoie le count caractĂšres de la chaĂźne s , en commençant par le caractĂšre avec le numĂ©ro start . L'indexation des caractĂšres vient de l'un.
  • '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/|()1{}[]?-_+~<>i!lI;:,"^. ' Est une chaĂźne contenant le" gradient "de" noir "( $ ) Ă  "Blanc" (espace) en caractĂšres ascii. TirĂ© du site http://paulbourke.net/dataformats/asciiart/ .
  • round(1 + max(0, min(66, v * 67))) - convertit les nombres rĂ©els de l'intervalle [0,1] en un entier dans la plage [1,67] pour prendre le personnage avec le numĂ©ro correspondant.

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


All Articles