Agachado nas sombras ou procurando o próximo mundo


Assembler é minha língua favorita ... mas a vida é tão curta.

Continuo o ciclo de pesquisa sobre a questão das sombras adequadas para alguns bagels. Após a publicação, esfriei uma e duas vezes para este tópico, mas o efeito de ação incompleta me leva a retornar aos restos de pixels e a concluir a gestalt .

Conhecendo a mim mesmo, tenho certeza de que o jogo dificilmente será concretizado, mas talvez o público esteja interessado nas minhas realizações nesse caminho espinhoso. E então vamos começar.

No final do último ciclo, cheguei ao entendimento de que o cálculo de gráficos em uma CPU já era o século passado, mas a obstinação natural insistia: nem todas as possibilidades eram usadas, ainda havia opções para soluções interessantes.

O traçado de raios permaneceu não implementado. Mais precisamente, seu tipo, onde, para cada pixel da imagem (bloco de pixels), um raio é lançado e o nível de iluminação do ponto atual é determinado. O algoritmo em si é descrito em um artigo anterior e não faz sentido retornar a ele. Para rastreamento de raio reverso, o código foi ainda mais simplificado, toda a trigonometria foi completamente removida, o que no futuro poderia dar um resultado aceitável.

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


Infelizmente, o resultado foi muito pior do que o esperado, valeu a pena implantar a imagem em tela cheia, o FPS procurou as unidades.



O agrupamento de pixels em macroblocos para reduzir os cálculos e a aplicação da suavização subsequente não melhoraram muito o desempenho. O efeito francamente não gostou da palavra.



O algoritmo era perfeitamente paralelo, mas não fazia sentido usar muitos fluxos, o efeito parecia muito pior do que no artigo anterior, mesmo com melhor qualidade de imagem.
Acabou sendo um beco sem saída. Eu tive que admitir, a CPU no cálculo de gráficos nos meus olhos se esgotou. A cortina.

Digressão 1
Na última década, praticamente não houve progresso no desenvolvimento de processadores de uso geral. Se abordado pelo usuário, o aumento máximo perceptível no desempenho não será superior a 30% por núcleo. O progresso, para dizer o mínimo, é insignificante. Se omitirmos a extensão do comprimento das instruções do vetor e alguma aceleração dos blocos transportadores, isso aumentará o número de núcleos de trabalho. O trabalho seguro com threads ainda é um prazer e nem todas as tarefas podem ser paralelizadas com êxito. Eu gostaria de ter um núcleo de trabalho, embora um, mas se for, é de 5 a 10 mais rápido, mas infelizmente e ah, como eles dizem.
Aqui em Habré, há uma excelente série de artigos "A vida na era do silício" escuro "", que explica alguns dos pré-requisitos para o estado atual das coisas, mas também retorna do céu para a terra. Na próxima década, você não pode esperar um aumento significativo na computação por núcleo. Mas podemos esperar um maior desenvolvimento do número de núcleos de GPU e sua aceleração geral. Mesmo no meu laptop antigo, o desempenho total estimado da GPU é 20 vezes maior que um único thread da CPU. Mesmo se você carregar efetivamente todos os quatro núcleos do processador, é muito menos do que gostaríamos.
Presto homenagem aos desenvolvedores dos gráficos do passado, que fizeram suas obras sem aceleradores de hardware, verdadeiros mestres.

Então, lidamos com a GPU. Acabou sendo um pouco inesperado para mim que, na prática real, poucas pessoas simplesmente espalhem polígonos em forma. Todas as coisas interessantes são criadas usando shaders . Tendo descartado os mecanismos 3D acabados, tentei estudar as miudezas da tecnologia em um nível profundo. Os mesmos processadores são o mesmo montador, apenas algumas instruções truncadas e suas próprias especificidades de trabalho. Para o teste, parei no GLSL , uma sintaxe do tipo C, simplicidade, muitas lições e exemplos de treinamento, incluindo o Habr.
Como eu costumava escrever em Pascal , a tarefa era como conectar o OpenGL
para o projeto. Consegui encontrar duas maneiras de conectar: ​​a biblioteca GLFW e o arquivo de cabeçalho dglOpenGL . A única coisa no começo não consegui conectar os shaders, mas aparentemente isso é pela curvatura das minhas mãos.

Digressão 2
Muitos amigos me perguntam por que escrevo em Pascal? Obviamente, esta é uma linguagem ameaçada, sua comunidade está caindo constantemente, quase não há desenvolvimento. Os engenheiros de sistema de baixo nível preferem C e Java, Python, Ruby ou o que estiver no auge no momento.
Para mim, Pascal é semelhante ao primeiro amor. Duas décadas atrás, nos dias de Turbo Pascal 5.5 , ele afundou em minha alma e tem andado comigo desde então, seja Delphi ou nos últimos anos Lázaro . Eu gosto da previsibilidade da linguagem, do nível relativamente baixo (o assembler insere e visualiza as instruções do processador), da compatibilidade com C. O principal é que o código é montado e executado sem problemas, mas o fato de não estar na moda está desatualizado e não há alguns recursos, isso não faz sentido. Dizem que existem pessoas que ainda escrevem no LISP , mas para ele em geral há meio século.

Então, vamos mergulhar no desenvolvimento. Para uma etapa de teste, não tomaremos modelos realistas precisos de sombreamento, mas tentaremos implementar o que já tentamos antes, mas com o desempenho da GPU, por assim dizer, para uma comparação clara.

Inicialmente, pensei em obter uma sombra aproximadamente dessa forma, usando triângulos para um objeto.



Para criar o efeito de um círculo suave, você precisa de muitos polígonos. Mas e se você usar triângulos no mínimo, usando um pixel shader para criar um buraco na forma. A ideia surgiu depois de ler um artigo de um mestre respeitado, no qual foi aberta a oportunidade de criar uma esfera com um sombreador.



Se você estender o triângulo além dos limites da tela, o resultado será o seguinte:



As bordas da sombra se mostraram muito rígidas e também pisaram. Mas há uma maneira de obter um resultado aceitável sem usar superamostragem , isso é usar bordas suavizadas. Para fazer isso, altere ligeiramente o esquema. Os cantos dos polígonos na interseção da tangente ao círculo ficarão transparentes.



O resultado é melhor, mas ainda parece antinatural.



Adicione um pouco de suavização do círculo para dar suavidade e também mude a forma do gradiente de linear para poder.



É um resultado aceitável.
E no final, adicionaremos objetos imitando obstáculos ao formulário.



Código de sombreador
//

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


Espero que tenha sido informativo

Seu humilde servo, atormentador de pixels, Rebuilder.

Estou anexando uma pequena demonstração . (EXE Windows)

PS O título do artigo contém um ovo de Páscoa , uma referência à trilogia Siala Chronicle . Um excelente trabalho nos estilos de fantasia, sobre os infortúnios dos chifres, de Alexei Pekhov.

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


All Articles