Comment créer un objectif grand angle / fisheye avec HLSL?

29

Quels sont les concepts à mettre en œuvre pour obtenir l'effet d'un objectif grand angle de différentes extrémités?

Un pseudocode et une explication spécifique faisant référence aux différentes étapes du pipeline de contenu, ainsi que les informations à transmettre du code source à HLSL seraient très utiles.

En outre, quelles sont les différences entre la mise en œuvre d'un objectif grand angle et un fisheye?

SirYakalot
la source

Réponses:

37

Un objectif grand angle ne doit pas se comporter différemment des autres modèles d'objectifs standard. Ils ont juste un FOV plus grand (dans le D3DXMatrixPerspectiveFovLHsens - je suppose que vous utilisez DirectX), ou des valeurs plus grandes gauche / droite et bas / haut (dans l'OpenGLglFrustum sens ).

Je crois que la partie vraiment intéressante réside dans la modélisation de l'objectif fisheye. Il y a Fisheye Quake que vous pouvez étudier, il vient avec la source.

La vraie projection fisheye

La projection d'un objectif fisheye est cependant très non linéaire. Dans le type d'objectif le plus courant (à ma connaissance, limité aux caméras de surveillance), un point Mdans l'espace est projeté sur la surface d'un hémisphère unitaire, puis cette surface subit une projection parallèle sur le disque unitaire:

           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

Il existe d'autres mappages fisheye qui peuvent donner des effets plus intéressants. C'est à vous.

Je peux voir deux façons de mettre en œuvre l'effet fisheye dans HLSL.

Méthode 1: effectuer la projection sur le vertex shader

Avantage : presque rien ne doit être changé dans le code. Le shader de fragment est extrêmement simple. Plutôt que:

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

Vous faites quelque chose comme ça (peut probablement être simplifié beaucoup):

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

Inconvénients : comme tout le pipeline de rendu a été pensé pour les transformations linéaires, la projection résultante est exacte pour les sommets, mais toutes les variations seront fausses, ainsi que les coordonnées de texture, et les triangles apparaîtront toujours comme des triangles même s'ils doivent apparaître déformés.

Solutions de contournement : il pourrait être acceptable d'obtenir une meilleure approximation en envoyant une géométrie raffinée au GPU, avec plus de subdivisions de triangle. Cela peut également être effectué dans un shader de géométrie, mais comme cette étape se produit après le vertex shader, le geometry shader serait assez complexe car il devrait effectuer ses propres projections supplémentaires.

Méthode 2: effectuez la projection sur le fragment shader

Une autre méthode consisterait à rendre la scène à l'aide d'une projection grand angle, puis à déformer l'image pour obtenir un effet fisheye à l'aide d'un ombrage de fragments en plein écran.

Si le point Ma des coordonnées (x,y)dans l'écran fisheye, cela signifie qu'il avait des coordonnées (x,y,z)sur la surface de l'hémisphère, avec z = sqrt(1-x*x-y*y). Ce qui signifie qu'il y avait des coordonnées (ax,ay)dans notre scène rendues avec un FOV de ce thetatype a = 1/(z*tan(theta/2)). (Je ne suis pas sûr à 100% de mes calculs ici, je vérifierai à nouveau ce soir).

Le fragment shader serait donc quelque chose comme ceci:

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

Avantage : vous obtenez une projection parfaite sans distorsions en dehors de celles dues à la précision des pixels.

Inconvénient : vous ne pouvez pas visualiser physiquement toute la scène, car le FOV ne peut pas atteindre 180 degrés. De plus, plus le champ de vision est grand, plus la précision au centre de l'image est mauvaise ... c'est précisément là que vous souhaitez une précision maximale.

Solutions de contournement : la perte de précision peut être améliorée en effectuant plusieurs passes de rendu, par exemple 5, et en effectuant la projection à la manière d'une carte de cube. Une autre solution de contournement très simple consiste à recadrer simplement l'image finale dans le champ de vision souhaité - même si l'objectif lui-même a un champ de vision à 180 degrés, vous souhaiterez peut-être n'en rendre qu'une partie. C'est ce qu'on appelle le fisheye "plein cadre" (ce qui est un peu ironique, car cela donne l'impression que vous obtenez quelque chose de "complet" alors qu'il recadre l'image).

(Remarque: si vous avez trouvé cela utile mais pas assez clair, dites-le moi, j'ai envie d'écrire un article plus détaillé à ce sujet).

sam hocevar
la source
très utile, et je serais ravi de l'article plus détaillé que vous souhaitez écrire de tout cœur!
SirYakalot
Serait-il possible de combiner les deux approches pour obtenir de meilleurs résultats? Faites d'abord la projection dans VS pour tout voir, puis déprogrammez dans PS et projetez à nouveau pour obtenir les UV corrects et tout? Il peut être nécessaire d'envoyer quelques paramètres supplémentaires à PS pour déprogrammer correctement l'original.
Ondrej Petrzilka
3

Ça devrait l'être z = sqrt(1.0 - in_Point.x * in_Point.x - in_Point.y * in_Point.y), non?

Mon implémentation GLSL est:

#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
la source
hey @Josh, comment fovTheta a été calculé?
Tom
1
Je n'ai édité cette réponse que pour ajuster la mise en forme, je pense que vous souhaitez vous adresser directement à @bman.
Josh