[C++/ALGO] Height Map -> Normal map

Bonjour tout le monde,

voilà mon problème :
Je souhaite à partir d’une height map (map en niveau de gris 8bits) creer une normal map (vecteur normaux encodées dans les valeurs RGB d’une image), jusque là, ça semble pas la mort :
je vais trouver le vecteur normal à partir du voisinage :

je fais une moyenne des pseudos vecteurs normaux des pixels du voisinage :


|||O| X : pixel actuel
||X|| O : vecteur normal acteul et O = ( x : 1 ; y : -1 ; z : valeur X - valeur de O )
|||_|

si z est négatif, on inverse le vecteur :
et O devient ( -1 ; 1 ; -(valeur X - valeur O) )
puis on normalise le vecteur (on réduit sa longueur à 1), et on l’ajoute au vecteur de X (qui au départ
est ( 0 ; 0 ; 1 )

Premier problème, c’est pas lisse (comme l’image d’en bas, mais en pire), pour améliorer le résultat j’ai déporté la normalisation à la fin avec une seule normalisation globale. Le résultat n’était gère mieux, donc jme suis dit plein d’entrain et de joie dans mon coeur, allez on va tenter d’appliquer des coeficients au vecteurs normaux :


|1|2|1|
|2|4|2|
|1|2|1|

Le résultat obtenu est le résultat suivant, mais c’est pas encore ça :

dans la doc de DirectX ils trouvent la normale en faisant la « différence centrale avec un matrice 3,3 »
seulement j’ai pas bien compris comment il faisait le central difference (même en passant du côté de MathWorld).

Donc voilà si quelqun aurait une idée pour améliorer l’aspect de la normal map, ou une autre de méthode de calcul pour les vecteurs, je prend :stuck_out_tongue:

EDIT : pas une bonne idée de poster à 2h du mat :stuck_out_tongue:

Euh … :stuck_out_tongue:

Essaie avec un Sobel filter, tu as un exemple de tool qui fait ça (source code inclus) sur le site d’ATI :
ATI Developer Tools ( NormalMapGenerator.zip )

Je ne sais pas si ça va t’aider beaucoup, mais voici ce que j’ai pu trouver sur les gradient operators:
http://www.aravind.ca/cs788h_Final_Project…_estimators.htm

\o/

Merci Bleuzaï pour l’exemple que je regarde avec grand interet, et merci Drealmer pour la théorie qui va avec :stuck_out_tongue:
D’après le 2eme lien j’ai apparemment utilisé la méthode de différence intermediaire qui donne les plus mauvais résultat.

Je serai curieux de voir le résultat final, simple curiosité :wink:

Intuitivement en faisant un programme tres tres con avec un sobel de ce dont je me rapelle comment ca marche moi j’obtiens ca. C’est sur mes souvenir alors ca peut etre tres tres approximatif. Cela dit c’est un peu nawak de faire ca soi meme a la main :stuck_out_tongue: y a des outils de partout poru le faire je pense, mais bon ca m’interessait de comprendre comment ca marche donc voila…

En utilisant un sobel
|-1 0 1 |
|-2 0 2 |
|-1 0 1 |
pour dX et son symetrique pour dY. Sur des doubles avec le gradient normalise au fur et a mesure et remappe en RGB 0…255 de -1…+1 a la fin (avec un petit boost pour voir mieux). Surement pas la meilleure methode pour eviter minimiser les erreurs, mais les c’est plus smooth deja je pense. Ca laisse les artefacts cela dit comme toi… il est aussi possible qu’ils proviennent d’une compression en JPG ou quoi auparavant de l’image. On montre la « derivee » (le gradient) de l’image la, c’est trop le genre de truc ou une compression a perte va montrer son derriere, surtout vu l’espacement regulier des artifacts en question… m’enfin bon… ptet que ca a rien a voir, a creuser en essayant d’autre images ou une generee soi meme en soft…

Je mets l’essentiel du code a l’arrache en C# tout naze (mon code, pas C# :P) qui va a 2 a l’heure (on peut faire une section unsafe et multiplier les perfs par 1000 mais comme c’etait juste pour tester je peux faire marcher d’abord et me faire iech avec les pointeurs apres :P) :

[codebox]
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;

namespace NormalizeMap
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private double ByteToDouble(byte a) {
return Convert.ToDouble(a) / 255.00;
}
private int DoubleToInt(double a)
{
return Convert.ToInt32(a * 255);
}
private void btnGo_Click(object sender, EventArgs e)
{
Bitmap bmp = this.pictureBox1.Image as Bitmap;
double[,] gradientX = new double[bmp.Width+1, bmp.Height+1];
double[,] gradientY = new double[bmp.Width+1, bmp.Height+1];
int[,] sobelY = new int[3, 3] { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
int[,] sobelX = new int[3, 3] { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };

        Conv3x3(bmp, gradientX, sobelX);
        Conv3x3(bmp, gradientY, sobelY);


        Bitmap result = this.pictureBox1.Image.Clone() as Bitmap;
        for (int y = 0; y < result.Height; y++)
        {
            for (int x = 0; x < result.Width; x++)
            {
                // 1. normalize dx, dy, 1
                double[] vector = new double[3] { gradientX[x, y], gradientY[x, y], 1 };
                Normalize(vector);
                int[] RGB = new int[3] { DoubleToInt(vector[0]), DoubleToInt(vector[1]), DoubleToInt(vector[2]) };
                MapTo0_255(RGB);
                Color color = Color.FromArgb(RGB[0], RGB[1], RGB[2]);
                result.SetPixel(x, y, color); 
            }
        }
        this.pictureBox2.Image = result;
        
    }

    private void MapTo0_255(int[] vector)
    {
        // clamp
        vector[0] = Math.Max(-255, vector[0]);
        vector[1] = Math.Max(-255, vector[1]);
        vector[2] = Math.Max(-255, vector[2]);
        vector[0] = Math.Min(255, vector[0]);
        vector[1] = Math.Min(255, vector[1]);
        vector[2] = Math.Min(255, vector[2]);
        // remap
        vector[0] = vector[0] / 2 + 128;
        vector[1] = vector[1] / 2 + 128;
        vector[2] = vector[2] / 2 + 128;
    }

    public void Normalize(double[] vector)
    {
        double norm = Math.Sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
        vector[0] = (vector[0] / norm);
        vector[1] = (vector[1] / norm);
        vector[2] = (vector[2] / norm);
    }



    public bool Conv3x3(Bitmap b, double[,] outputGradient, int[,] sobel)
    {
        for (int y = 1; y < b.Height - 1; y++)
        {
            for (int x = 1; x < b.Width - 1; x++)
            {
                outputGradient[x,y] =  ByteToDouble(b.GetPixel(x-1,y-1).R) * sobel[0, 0] +
                                                 ByteToDouble(b.GetPixel(x , y - 1).R) * sobel[1, 0] +
                                                ByteToDouble(b.GetPixel(x + 1, y - 1).R) * sobel[2, 0] +
                                                 ByteToDouble(b.GetPixel(x - 1, y).R) * sobel[0, 1] +
                                                 ByteToDouble(b.GetPixel(x , y ).R) * sobel[1, 1] +
                                                 ByteToDouble(b.GetPixel(x + 1, y ).R) * sobel[2, 1] +
                                                 ByteToDouble(b.GetPixel(x - 1, y + 1).R) * sobel[0, 2] +
                                                 ByteToDouble(b.GetPixel(x , y + 1).R) * sobel[1, 2] +
                                                 ByteToDouble(b.GetPixel(x + 1, y + 1).R) * sobel[2, 2];
                outputGradient[x, y] *= 5; // SUPER BOOOST!!!!

            }
        }

        

        return true;
    }
}

}
[/codebox]

Le codebehind des fois que tu voudrais recompiler:
[codebox]
namespace NormalizeMap
{
partial class Form1
{
///


/// Required designer variable.
///

private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.pictureBox1 = new System.Windows.Forms.PictureBox();
        this.btnGo = new System.Windows.Forms.Button();
        this.pictureBox2 = new System.Windows.Forms.PictureBox();
        ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
        ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).BeginInit();
        this.SuspendLayout();
        // 
        // pictureBox1
        // 
        this.pictureBox1.Image = global::NormalizeMap.Properties.Resources.badmap;
        this.pictureBox1.Location = new System.Drawing.Point(12, 12);
        this.pictureBox1.Name = "pictureBox1";
        this.pictureBox1.Size = new System.Drawing.Size(251, 254);
        this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
        this.pictureBox1.TabIndex = 0;
        this.pictureBox1.TabStop = false;
        // 
        // btnGo
        // 
        this.btnGo.Location = new System.Drawing.Point(12, 272);
        this.btnGo.Name = "btnGo";
        this.btnGo.Size = new System.Drawing.Size(508, 23);
        this.btnGo.TabIndex = 1;
        this.btnGo.Text = "Go!";
        this.btnGo.UseVisualStyleBackColor = true;
        this.btnGo.Click += new System.EventHandler(this.btnGo_Click);
        // 
        // pictureBox2
        // 
        this.pictureBox2.Location = new System.Drawing.Point(269, 12);
        this.pictureBox2.Name = "pictureBox2";
        this.pictureBox2.Size = new System.Drawing.Size(251, 254);
        this.pictureBox2.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
        this.pictureBox2.TabIndex = 2;
        this.pictureBox2.TabStop = false;
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(527, 304);
        this.Controls.Add(this.pictureBox2);
        this.Controls.Add(this.btnGo);
        this.Controls.Add(this.pictureBox1);
        this.Name = "Form1";
        this.Text = "Form1";
        ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
        ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).EndInit();
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.PictureBox pictureBox1;
    private System.Windows.Forms.Button btnGo;
    private System.Windows.Forms.PictureBox pictureBox2;
}

}

[/codebox]

Si j’ai fait plein d’erreurs ou si ca rame puissance 1000000 pas la peine de raler :stuck_out_tongue:

Oui si je genere l’image de gauche moi meme (c’est pas la meme, moi c’est la norme de x et y, toi c’est une gaussienne, mais bon, whatever) avec l’erreur, la quantization et tout, qui va avec j’ai pas les petits ronds zarbi presents au dessus:

Ayé ça marche, je posterai ce soir le logiciel utilisé pour faire tout ça (comme ça tout le monde pourra faire ses screenshots =)), après avoir corrigé 2-3 trucs.

Je suis passé sur une matrice sobel aussi, le résultat est bien meilleur

Je viens de me rendre compte que si on zoome à mort sur l’image grayscale de départ fournie par TwinSidE on peut voir les petits ronds à l’oeil nu… Va falloir revoir le code qui fait la gaussienne :stuck_out_tongue:

Drealmer > C’est pas une gaussienne à la base :stuck_out_tongue:
c’est juste une équation d’elipse trafiquée, sur laquelle je passe une correction Gamma pour obtenir l’aspect voulu

Pour ceux qui veulent voir le résultat, voici le logiciel qui le permet : ici.
C’est un générateur de texture paramétrique en 256*256, certaines « briques » ne sont pas encore finalisées (notemment l’illumination).

Pour utiliser l’interface il faut le .net 2, mais si vous voulez, il y a possibilité de tenter en entrant des valeurs au pif dans un éditeur hexa et de lancer le générateur. :stuck_out_tongue:

EDIT : ah ouais on les voi bien quand même les ronds :stuck_out_tongue: