¿Cómo creo una lente gran angular / ojo de pez con HLSL?

29

¿Cuáles son los conceptos que deben implementarse para lograr el efecto de una lente gran angular de extremidades variables?

Sería muy útil el pseudocódigo y la explicación específica que se refieren a las diversas etapas de la canalización de contenido, así como a la información que se debe pasar del código fuente al HLSL.

Además, ¿cuáles son las diferencias entre implementar una lente gran angular y un ojo de pez?

Sir Yakalot
fuente

Respuestas:

37

Una lente gran angular no debe comportarse de manera diferente que otros modelos de lentes normales. Solo tienen un FOV más grande (en el D3DXMatrixPerspectiveFovLHsentido, supongo que usa DirectX), o valores más grandes a la izquierda / derecha e inferior / superior (en el glFrustumsentido de OpenGL ).

Creo que la parte realmente interesante radica en modelar la lente ojo de pez. Hay Fisheye Quake que puedes estudiar, viene con la fuente.

La verdadera proyección de ojo de pez

La proyección de una lente ojo de pez, sin embargo, es altamente no lineal. En el tipo de lente más común (que yo sepa, que se limita a las cámaras de vigilancia), Mse proyecta un punto en el espacio sobre la superficie del hemisferio de una unidad, luego esa superficie se proyecta paralelamente en el disco de la unidad:

           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

Hay otras asignaciones de ojo de pez que pueden dar efectos más interesantes. Tu decides.

Puedo ver dos formas de implementar el efecto ojo de pez en HLSL.

Método 1: realice la proyección en el sombreador de vértices

Ventaja : casi nada necesita ser cambiado en el código. El sombreador de fragmentos es extremadamente simple. Más bien que:

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

Haces algo como esto (probablemente se puede simplificar mucho):

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

Inconvenientes : dado que toda la tubería de renderizado se pensó para transformaciones lineales, la proyección resultante es exacta para vértices, pero todas las variaciones serán incorrectas, así como las coordenadas de textura, y los triángulos seguirán apareciendo como triángulos, aunque deberían aparecer distorsionados.

Soluciones alternativas : podría ser aceptable obtener una mejor aproximación enviando una geometría refinada a la GPU, con más subdivisiones de triángulos. Esto también se puede realizar en un sombreador de geometría, pero dado que este paso ocurre después del sombreador de vértices, el sombreador de geometría sería bastante complejo porque tendría que realizar sus propias proyecciones adicionales.

Método 2: realice la proyección en el sombreador de fragmentos

Otro método sería renderizar la escena usando una proyección de gran angular, luego distorsionar la imagen para lograr un efecto de ojo de pez usando un sombreador de fragmentos de pantalla completa.

Si el punto Mtiene coordenadas (x,y)en la pantalla de ojo de pez, significa que tenía coordenadas (x,y,z)en la superficie del hemisferio, con z = sqrt(1-x*x-y*y). Lo que significa que tenía coordenadas (ax,ay)en nuestra escena renderizadas con un FOV de thetatal a = 1/(z*tan(theta/2)). (No estoy 100% seguro de mis cálculos aquí, lo revisaré nuevamente esta noche).

Por lo tanto, el sombreador de fragmentos sería algo así:

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

Ventaja : obtienes una proyección perfecta sin distorsiones aparte de las debidas a la precisión de los píxeles.

Inconveniente : no puede ver físicamente toda la escena, ya que el FOV no puede alcanzar los 180 grados. Además, cuanto más grande sea el FOV, peor será la precisión en el centro de la imagen ... que es precisamente donde desea la máxima precisión.

Soluciones alternativas : la pérdida de precisión se puede mejorar realizando varias pasadas de representación, por ejemplo 5, y haciendo la proyección a la manera de un mapa de cubos. Otra solución muy simple es simplemente recortar la imagen final al FOV deseado; incluso si la lente en sí tiene un FOV de 180 grados, es posible que desee representar solo una parte de ella. Esto se llama ojo de pez "fotograma completo" (lo cual es un poco irónico, ya que da la impresión de que obtienes algo "completo" mientras en realidad recorta la imagen).

(Nota: si lo encuentra útil pero no lo suficientemente claro, dígame, tengo ganas de escribir un artículo más detallado sobre esto).

sam hocevar
fuente
muy útil, y agradecería el artículo más detallado que desea escribir de todo corazón!
SirYakalot
¿Sería posible combinar ambos enfoques para obtener mejores resultados? Primero, haga la proyección en VS para tener todo a la vista y luego desproyecte en PS y proyecte nuevamente para obtener los rayos UV correctos y todo. Puede ser necesario enviar algunos parámetros más a PS para desproyectarlos correctamente al original.
Ondrej Petrzilka
3

Debería ser z = sqrt(1.0 - in_Point.x * in_Point.x - in_Point.y * in_Point.y), ¿verdad?

Mi implementación GLSL es:

#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
fuente
Hola @ Josh, ¿cómo se calculó fovTheta?
tom
1
Solo edité esta respuesta para ajustar el formato, creo que desea dirigirse a @bman directamente.
Josh