Accroupi dans l'ombre ou à la recherche du prochain monde


L'assembleur est ma langue préférée ... mais la vie est si courte.

Je continue le cycle de recherche sur la question des ombres appropriées pour certains bagels. Après la publication, je me suis refroidi une fois et deux pour ce sujet, mais l'effet d'une action incomplète m'incite à revenir aux débris de pixels et à terminer la gestalt .

En me connaissant, je suis sûr que le jeu ne sera guère incarné, mais peut-être qu'une partie du public sera intéressé par mes réalisations sur ce chemin épineux. Et donc commençons.

À la fin du dernier cycle, j'ai compris que le calcul des graphiques sur un processeur était déjà le siècle dernier, mais l'obstination naturelle insistait: toutes les possibilités n'étaient pas utilisées, il y avait encore des options pour des solutions intéressantes.

Le tracé de rayons est resté non implémenté. Plus précisément, son genre, où pour chaque pixel de l'image (bloc de pixels) un rayon est lancé et le niveau d'éclairement du point courant est déterminé. L'algorithme lui-même est décrit dans un article précédent et il n'y a aucun intérêt à y revenir. Pour le traçage inverse, le code a été encore plus simplifié, toute la trigonométrie a été complètement supprimée, ce qui pourrait à l'avenir donner un résultat acceptable.

Pascal
const tile_size = 32; //   tile_size1 : single = 0.03125; // 1/32 -    block_size = 4; // /    Size_X:Byte = 32; //    X Size_Y:Byte = 24; //    Y //--------------------------------- function is_no_empty(x,y:Integer):Integer; begin if (x>=0) AND (x<Size_X) AND (y>=0) AND (y<Size_Y) then begin if map[x,y]=1 then begin is_no_empty:=1; end else if map[x,y]=2 then begin is_no_empty:=2; end else is_no_empty:=0; end else is_no_empty:=-1; end; //--------------------------------- function crossing(r_view, x,y:Single; xi,yj, i,j:Integer):Byte; var di,dj,ddi,ddj :Shortint; //   k,i2,j2 :integer; //    key:Boolean; last_k, transp_key :Byte; sum_lenX,sum_lenY, Dx,Dy,Dx1,DY1, l :Single; //  sec1,cosec1, temp_x,temp_y, dx0,dy0 :Single; //   i0,j0 :Integer; //       begin temp_x := i*block_size; temp_y := j*block_size; i0 := trunc(temp_x * tile_size1); j0 := trunc(temp_y * tile_size1); l := sqrt(sqr(temp_y-y) + sqr(temp_x-x)) + 0.0000001; transp_key := 0; //     if is_no_empty(xi,yj)>0 then inc(transp_key); if (xi=i0) and (yj=j0) then begin crossing := min(255,transp_key*64+ l * r_view); exit; end; dx0 := (temp_x-x)/l+0.0000001; dy0 := (temp_y-y)/l+0.0000001; key := False; last_k :=0; //   if dx0<0 then begin di :=-1; ddi:= 0; end else begin di := 1; ddi:= 1; end; if dy0<0 then begin dj :=-1; ddj:= 0; end else begin dj := 1; ddj:= 1; end; sum_lenX := 0; sum_lenY := 0; sec1 := 1/dx0; cosec1 := 1/dy0; //       Y temp_x := x-(xi+ddi) * tile_size ; temp_y := y-(yj+ddj) * tile_size ; Dx := sqrt(sqr(temp_x) + sqr(temp_x * sec1 * dy0)); DY := sqrt(sqr(temp_y) + sqr(temp_y * cosec1 * dx0)); //      Y Dx1 := abs(tile_size * sec1); Dy1 := abs(tile_size * cosec1); repeat if sum_lenX+DX < sum_lenY+DY then begin xi += di; k := is_no_empty(xi,yj); sum_lenX += DX; if DX<>Dx1 then DX := Dx1; end else begin yj += dj; k := is_no_empty(xi,yj); sum_lenY += DY; if DY<>Dy1 then DY := Dy1; end; if key Then begin if (xi<>i2) Or (yj<>j2) then begin //  (  ) if last_k=1 then begin crossing := 255; exit; end; //   (  ) if transp_key>2 then begin crossing := 255; exit; end; inc(transp_key); key:= false; end; end; if k>0 then begin i2:=xi; j2:=yj; key:=true; last_k:=k; end; //    if (xi=i0) and (yj=j0) then begin crossing := min(255, transp_key*64+ l * r_view); exit; end; until k=-1; //     end; //--------------------------------- .................. x0:= mouse_x; y0:= mouse_y; //       x1 := x0 div tile_size; y1 := y0 div tile_size; koef := tile_size div block_size; //      (     ) for j:=0 to Size_Y * koef do for i:=0 to Size_X * koef do picture_mask.SetPixel(i, j, BGRA(0,0,0,crossing(x0, y0, x1, y1, i, j))); .................. 


Hélas, le résultat a été bien pire que prévu, cela valait la peine de déployer l'image en plein écran, FPS a cherché des unités.



Le regroupement des pixels en macroblocs pour réduire les calculs et appliquer le lissage ultérieur n'a pas beaucoup amélioré les performances. L'effet n'a franchement pas aimé le mot du tout.



L'algorithme était parfaitement parallèle, mais cela n'avait pas de sens d'utiliser beaucoup de flux, l'effet semblait bien pire que dans l'article précédent, même avec une meilleure qualité d'image.
Cela s'est avéré être une impasse. Je devais l'admettre, le CPU dans le calcul des graphismes à mes yeux s'est épuisé. Le rideau.

Digression 1
Au cours de la dernière décennie, il n'y a eu pratiquement aucun progrès dans le développement de processeurs à usage général. Si approché par l'utilisateur, l'augmentation de performance maximale perceptible ne dépasse pas 30% par cœur. Le progrès, pour le moins, est insignifiant. Si nous omettons l'extension de la longueur des instructions vectorielles et une certaine accélération des blocs convoyeurs, il s'agit d'une augmentation du nombre de cœurs de travail. Travailler en toute sécurité avec des threads est toujours un plaisir, et toutes les tâches ne peuvent pas être parallélisées avec succès. Je voudrais avoir un noyau qui fonctionne, quoique un, mais si c'est le cas, il est 5-10 plus rapide, mais hélas et oh, comme on dit.
Ici, sur Habré, il y a une excellente série d'articles "La vie à l'ère du silicium" sombre "", qui explique certaines des conditions préalables à la situation actuelle, mais qui revient aussi du ciel à la terre. Au cours de la prochaine décennie, vous ne pouvez pas vous attendre à une augmentation significative de l'informatique par cœur. Mais nous pouvons nous attendre à un développement supplémentaire du nombre de cœurs GPU et de leur accélération globale. Même sur mon ancien ordinateur portable, les performances GPU totales estimées sont 20 fois supérieures à celles d'un seul thread CPU. Même si vous chargez efficacement les 4 cœurs de processeur, c'est beaucoup moins que nous le souhaiterions.
Je rends hommage aux développeurs des graphismes du passé, qui ont fait leurs chefs-d'œuvre sans accélérateurs matériels, de vrais maîtres.

Donc, nous traitons avec le GPU. Il s'est avéré quelque peu inattendu pour moi que dans la pratique, peu de gens dispersent simplement des polygones en forme. Toutes les choses intéressantes sont créées à l'aide de shaders . Après avoir jeté les moteurs 3D finis, j'ai essayé d'étudier les abats de la technologie telle qu'elle est à un niveau profond. Les mêmes processeurs sont le même assembleur, seulement quelques instructions tronquées et leurs spécificités de travail. Pour le test, je me suis arrêté à GLSL , une syntaxe de type C, la simplicité, beaucoup de leçons de formation et d'exemples, y compris le Habr.
Comme j'étais surtout habitué à écrire en Pascal , la tâche était de savoir comment connecter OpenGL
au projet. J'ai réussi à trouver deux façons de me connecter: la bibliothèque GLFW et le fichier d'en-tête dglOpenGL . La seule chose dans le premier, je n'ai pas pu connecter les shaders, mais apparemment, c'est de la courbure de mes mains.

Digression 2
Beaucoup d'amis me demandent pourquoi j'écris en Pascal? De toute évidence, c'est une langue en voie de disparition, sa communauté est en baisse constante, il n'y a presque pas de développement. Les ingénieurs système de bas niveau préfèrent C et Java, Python, Ruby ou tout ce qui est à leur apogée en ce moment.
Pour moi, Pascal s'apparente au premier amour. Il y a deux décennies, à l'époque de Turbo Pascal 5.5 , il a coulé dans mon âme et marche avec moi depuis, que ce soit Delphi ou ces dernières années Lazarus . J'aime la prévisibilité du langage, le niveau relativement bas (inserts d'assembleur et affichage des instructions du processeur), la compatibilité avec C.L'essentiel est que le code soit assemblé et exécuté sans problème, mais le fait qu'il ne soit pas à la mode est obsolète et qu'il n'y ait pas de fonctionnalités, c'est absurde. La rumeur veut qu'il y ait des gens qui écrivent toujours sur LISP , mais pour lui en général depuis un demi-siècle.

Plongeons-nous donc dans le développement. Pour l'étape de test, nous ne prendrons pas de modèles d'occultation réalistes précis, mais essayerons de mettre en œuvre ce que nous avons déjà essayé auparavant, mais avec des performances GPU, pour ainsi dire pour une comparaison claire.

Au départ, j'ai pensé à obtenir une ombre d'environ cette forme, en utilisant des triangles pour un objet.



Pour créer l'effet d'un cercle lisse, vous avez besoin de beaucoup de polygones. Mais que faire si vous utilisez des triangles au minimum, en utilisant un pixel shader pour créer un trou dans la forme. L'idée m'est venue après avoir lu un article d'un maître respecté, dans lequel l'opportunité s'est ouverte de créer une sphère avec un shader.



Si vous étendez le triangle au-delà des limites de l'écran, le résultat est le suivant:



Les bordures de l'ombre se sont avérées très rigides et également étagées. Mais il existe un moyen d'obtenir un résultat acceptable sans utiliser de suréchantillonnage , c'est-à-dire en utilisant des bordures lissées. Pour ce faire, modifiez légèrement le schéma. Les coins des polygones à l'intersection de la tangente au cercle seront transparents.



Le résultat est meilleur, mais il ne semble toujours pas naturel.



Ajoutez un peu de lissage du cercle pour donner de la douceur, et changez également la forme du dégradé de linéaire en puissance.



C'est un résultat acceptable.
Et à la fin, nous ajouterons des objets imitant les obstacles au formulaire.



Code de shader
//

#version 330 core
layout (location = 0) in vec2 aVertexPosition;
void main(void) {
gl_Position = vec4(aVertexPosition.xy, 0, 1.0);
}

//

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
uniform mat4 uModelViewMatrix;
uniform float uRadius;
uniform vec2 uHeroPoint;
out float fTransparency;
out vec2 vCenter;
void main(){
vCenter = gl_in[0].gl_Position.xy;
vec2 d = uHeroPoint - vCenter;
float l = length(d);
float i = uRadius / l;
float ii = i*i;
float ij = i * sqrt(1 - ii);
vec2 p1 = vec2(vCenter.x + dx*ii - dy*ij , vCenter.y + dx*ij + dy*ii);
vec2 p2 = vec2(vCenter.x + dx*ii + dy*ij , vCenter.y - dx*ij + dy*ii);
d = uHeroPoint - p1;
vec2 p3 = vec2(p1 - d/length(d)*1000000);
d = uHeroPoint - p2;
vec2 p4 = vec2(p2 - d/length(d)*1000000);
fTransparency = 0;
gl_Position = uModelViewMatrix * vec4(p1, 0, 1);
EmitVertex();
fTransparency = 1;
gl_Position = uModelViewMatrix * vec4(p3, 0, 1);
EmitVertex();
gl_Position = uModelViewMatrix * vec4(vCenter, 0, 1);
EmitVertex();
gl_Position = uModelViewMatrix * vec4(p4, 0, 1);
EmitVertex();
fTransparency = 0;
gl_Position = uModelViewMatrix * vec4(p2, 0, 1);
EmitVertex();
EndPrimitive();
}

//

#version 330 core
precision mediump float;
varying float fTransparency;
varying vec2 vCenter;
uniform float uRadius;
uniform vec2 uScreenHalfSize;
uniform float uShadowTransparency;
uniform float uShadowSmoothness;
out vec4 FragColor;
void main(){
float l = distance(vec2((gl_FragCoord.xy - uScreenHalfSize.xy)/uScreenHalfSize.y), vCenter.xy);
if (l<uRadius) {discard;}
else {FragColor = vec4(0, 0, 0, min(pow(fTransparency, uShadowSmoothness), (l-uRadius)/uRadius*10)*uShadowTransparency);}
}


J'espère que c'était instructif

Votre humble serviteur, bourreau de pixels, reconstructeur.

Je joins une petite démo . (EXE Windows)

PS Le titre de l'article contient un œuf de Pâques , une référence à la trilogie Siala Chronicle . Un excellent travail dans les styles de fantaisie, sur les malheurs des cornes, d'Alexei Pekhov.

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


All Articles