Como crio uma lente grande angular / olho de peixe com HLSL?

29

Quais são os conceitos que precisam ser implementados para alcançar o efeito de uma lente grande angular de extremidades variadas?

Pseudocódigo e explicação específica referentes aos vários estágios do pipeline de conteúdo, bem como quais informações precisam ser passadas do código-fonte para o HLSL, seriam muito úteis.

Além disso, quais são as diferenças entre implementar uma lente grande angular e uma lente olho de peixe?

SirYakalot
fonte

Respostas:

37

Uma lente grande angular não deve se comportar de maneira diferente de outros modelos comuns. Eles apenas têm um FOV maior (no D3DXMatrixPerspectiveFovLHsentido - suponho que você use o DirectX) ou valores maiores para a esquerda / direita e inferior / superior (no glFrustumsentido OpenGL ).

Eu acredito que a parte realmente interessante está na modelagem da lente olho de peixe. Há o Fisheye Quake que você pode estudar, ele vem com a fonte.

A verdadeira projeção olho de peixe

A projeção de uma lente olho de peixe, no entanto, é altamente não linear. No tipo de lente mais comum (que eu saiba, que é limitado às câmeras de vigilância), um ponto Mno espaço é projetado na superfície de um hemisfério unitário e, em seguida, essa superfície sofre uma projeção paralela no disco unitário:

           M
             x                 M: world position
              \                M': projection of M on the unit hemisphere
               \  ______       M": projection of M' on the unit disc (= the screen)
             M'_x'      `-.
             ,' |\         `.
            /   | \          \
           /    |  \          \
          |     |   \          |
__________|_____|____\_________|_________
                M"    O        1

Existem outros mapeamentos de olho de peixe que podem dar efeitos mais interessantes. Você decide.

Eu posso ver duas maneiras de implementar o efeito olho de peixe no HLSL.

Método 1: executar a projeção no vertex shader

Vantagem : quase nada precisa ser alterado no código. O shader de fragmento é extremamente simples. Ao invés de:

...
float4 screenPoint = mul(worldPoint, worldViewProjMatrix);
...

Você faz algo assim (provavelmente pode ser muito simplificado):

...
// This is optional, but it computes the z coordinate we will
// need later for Z sorting.
float4 out_Point = mul(in_Point, worldViewProjMatrix);

// This retrieves the world position so that we can project on
// the hemisphere. Normalise this vector and you get M'
float4 tmpPoint = mul(in_Point, worldViewMatrix);

// This computes the xy coordinates of M", which happen to
// be the same as M'.
out_Point.xy = tmpPoint.xy / length(tmpPoint.xyz);
...

Desvantagens : como todo o pipeline de renderização foi pensado para transformações lineares, a projeção resultante é exata para vértices, mas todas as variações estarão erradas, assim como as coordenadas de textura, e os triângulos ainda aparecerão como triângulos, mesmo que pareçam distorcidos.

Soluções alternativas : seria aceitável obter uma melhor aproximação enviando uma geometria refinada para a GPU, com mais subdivisões de triângulo. Isso também pode ser realizado em um sombreador de geometria, mas como essa etapa ocorre após o sombreador de vértice, o sombreador de geometria seria bastante complexo porque precisaria executar suas próprias projeções adicionais.

Método 2: executar a projeção no shader de fragmento

Outro método seria renderizar a cena usando uma projeção de grande angular e distorcer a imagem para obter um efeito olho de peixe usando um sombreador de fragmento de tela cheia.

Se o ponto Mtiver coordenadas (x,y)na tela olho de peixe, significa que ele possui coordenadas (x,y,z)na superfície do hemisfério, com z = sqrt(1-x*x-y*y). O que significa que havia coordenadas (ax,ay)em nossa cena processadas com um FOV de thetatal modo a = 1/(z*tan(theta/2)). (Não tenho 100% de certeza da minha matemática aqui, vou verificar novamente hoje à noite).

O shader de fragmento seria, portanto, algo como isto:

void main(in float4 in_Point : POSITION,
          uniform float u_Theta,
          uniform sampler2D u_RenderBuffer,
          out float4 out_FragColor : COLOR)
{
    z = sqrt(1.0 - in_Point.x * in_Point.x - in_Point.y * in_Point.y);
    float a = 1.0 / (z * tan(u_Theta * 0.5));
    out_FragColor = tex2D(u_RenderBuffer, (in_Point.xy - 0.5) * 2.0 * a);
}

Vantagem : você obtém uma projeção perfeita, sem distorções, além daquelas devido à precisão do pixel.

Desvantagem : você não pode visualizar fisicamente a cena inteira, pois o FOV não pode atingir 180 graus. Além disso, quanto maior o FOV, pior a precisão no centro da imagem ... que é exatamente onde você deseja a máxima precisão.

Soluções alternativas : a perda de precisão pode ser aprimorada executando várias passagens de renderização, por exemplo 5, e fazendo a projeção da maneira de um mapa de cubo. Outra solução muito simples é simplesmente cortar a imagem final para o FOV desejado - mesmo que a lente tenha um FOV de 180 graus, você pode renderizar apenas uma parte dela. Isso é chamado de olho de peixe "full-frame" (que é meio irônico, pois dá a impressão de que você obtém algo "cheio" enquanto realmente corta a imagem).

(Nota: se você achou isso útil, mas não suficientemente claro, diga-me, tenho vontade de escrever um artigo mais detalhado sobre isso).

sam hocevar
fonte
muito útil e gostaria de receber o artigo mais detalhado que você deseja escrever de todo o coração!
perfil completo de SirYakalot
Seria possível combinar as duas abordagens para obter melhores resultados? Primeiro faça a projeção no VS para exibir tudo e depois o projeto no PS e projete novamente para obter UVs corretos e tudo mais? Pode ser necessário enviar mais alguns parâmetros ao PS para não projetar corretamente o original.
Ondrej Petrzilka 21/01
3

Deveria ser z = sqrt(1.0 - in_Point.x * in_Point.x - in_Point.y * in_Point.y), certo?

Minha implementação GLSL é:

#ifdef GL_ES
precision mediump float;
#endif

varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float fovTheta; // FOV's theta

// fisheye
void main (void)
{   
    vec2 uv = v_texCoord - 0.5;
    float z = sqrt(1.0 - uv.x * uv.x - uv.y * uv.y);
    float a = 1.0 / (z * tan(fovTheta * 0.5));
//  float a = (z * tan(fovTheta * 0.5)) / 1.0; // reverse lens
    gl_FragColor = texture2D(u_texture, (uv* a) + 0.5);
}
bman
fonte
hey @ Jos, como fovTheta foi calculado?
tom
1
Eu apenas editei esta resposta para ajustar a formatação, acredito que você deseja endereçar @bman diretamente.
21712 Josh